diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/packages/mesh/src/MeshGeometry.js b/packages/mesh/src/MeshGeometry.js new file mode 100644 index 0000000..ae6c702 --- /dev/null +++ b/packages/mesh/src/MeshGeometry.js @@ -0,0 +1,61 @@ +import { TYPES } from '@pixi/constants'; +import { Buffer, Geometry } from '@pixi/core'; + +/** + * Standard 2D geometry used in PixiJS. + * + * Geometry can be defined without passing in a style or data if required. + * + * ```js + * const geometry = new PIXI.Geometry(); + * + * geometry.addAttribute('positions', [0, 0, 100, 0, 100, 100, 0, 100], 2); + * geometry.addAttribute('uvs', [0,0,1,0,1,1,0,1], 2); + * geometry.addIndex([0,1,2,1,3,2]); + * + * ``` + * @class + * @memberof PIXI + * @extends PIXI.Geometry + */ +export default class MeshGeometry extends Geometry +{ + /** + * @param {Float32Array|number[]} vertices - Positional data on geometry. + * @param {Float32Array|number[]} uvs - Texture UVs. + * @param {Uint16Array|number[]} index - IndexBuffer + */ + constructor(vertices, uvs, index) + { + super(); + + const verticesBuffer = new Buffer(vertices); + const uvsBuffer = new Buffer(uvs, true); + const indexBuffer = new Buffer(index, true, true); + + this.addAttribute('aVertexPosition', verticesBuffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', uvsBuffer, 2, false, TYPES.FLOAT) + .addIndex(indexBuffer); + + /** + * Dirty flag to limit update calls on Mesh. For example, + * limiting updates on a single Mesh instance with a shared Geometry + * within the render loop. + * @private + * @member {number} + * @default -1 + */ + this._updateId = -1; + } + + /** + * If the vertex position is updated. + * @member {number} + * @readonly + * @private + */ + get vertexDirtyId() + { + return this.buffers[0]._updateID; + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/packages/mesh/src/MeshGeometry.js b/packages/mesh/src/MeshGeometry.js new file mode 100644 index 0000000..ae6c702 --- /dev/null +++ b/packages/mesh/src/MeshGeometry.js @@ -0,0 +1,61 @@ +import { TYPES } from '@pixi/constants'; +import { Buffer, Geometry } from '@pixi/core'; + +/** + * Standard 2D geometry used in PixiJS. + * + * Geometry can be defined without passing in a style or data if required. + * + * ```js + * const geometry = new PIXI.Geometry(); + * + * geometry.addAttribute('positions', [0, 0, 100, 0, 100, 100, 0, 100], 2); + * geometry.addAttribute('uvs', [0,0,1,0,1,1,0,1], 2); + * geometry.addIndex([0,1,2,1,3,2]); + * + * ``` + * @class + * @memberof PIXI + * @extends PIXI.Geometry + */ +export default class MeshGeometry extends Geometry +{ + /** + * @param {Float32Array|number[]} vertices - Positional data on geometry. + * @param {Float32Array|number[]} uvs - Texture UVs. + * @param {Uint16Array|number[]} index - IndexBuffer + */ + constructor(vertices, uvs, index) + { + super(); + + const verticesBuffer = new Buffer(vertices); + const uvsBuffer = new Buffer(uvs, true); + const indexBuffer = new Buffer(index, true, true); + + this.addAttribute('aVertexPosition', verticesBuffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', uvsBuffer, 2, false, TYPES.FLOAT) + .addIndex(indexBuffer); + + /** + * Dirty flag to limit update calls on Mesh. For example, + * limiting updates on a single Mesh instance with a shared Geometry + * within the render loop. + * @private + * @member {number} + * @default -1 + */ + this._updateId = -1; + } + + /** + * If the vertex position is updated. + * @member {number} + * @readonly + * @private + */ + get vertexDirtyId() + { + return this.buffers[0]._updateID; + } +} diff --git a/packages/mesh/src/MeshMaterial.js b/packages/mesh/src/MeshMaterial.js new file mode 100644 index 0000000..028cf49 --- /dev/null +++ b/packages/mesh/src/MeshMaterial.js @@ -0,0 +1,130 @@ +import { Shader, Program, TextureMatrix } from '@pixi/core'; +import vertex from './shader/mesh.vert'; +import fragment from './shader/mesh.frag'; +import { Matrix } from '@pixi/math'; +import { premultiplyTintToRgba } from '@pixi/utils'; + +/** + * Slightly opinionated default shader for PixiJS 2D objects. + * @class + * @memberof PIXI + * @extends PIXI.Shader + */ +export default class MeshMaterial extends Shader +{ + /** + * @param {PIXI.Texture} uSampler - Texture that material uses to render. + * @param {object} [options] - Additional options + * @param {number} [options.alpha=1] - Default alpha. + * @param {number} [options.tint=0xFFFFFF] - Default tint. + */ + constructor(uSampler, options) + { + const program = Program.from(vertex, fragment); + + const uniforms = { + uSampler, + alpha: 1, + uTextureMatrix: Matrix.IDENTITY, + uColor: new Float32Array([1, 1, 1, 1]), + }; + + super(program, uniforms); + + /** + * Only do update if tint or alpha changes. + * @member {boolean} + * @private + * @default false + */ + this._colorDirty = false; + + /** + * TextureMatrix instance for this Mesh, used to track Texture changes + * + * @member {PIXI.TextureMatrix} + * @readonly + */ + // TODO get this back in! + this.uvMatrix = new TextureMatrix(this.uSampler); + + /** + * `true` if shader can be batch with the renderer's batch system. + * @member {boolean} + * @default true + */ + this.batchable = true; + + // Set defaults + const { tint, alpha } = Object.assign({ + tint: 0xFFFFFF, + alpha: 1, + }, options); + + this.tint = tint; + this.alpha = alpha; + } + + /** + * Reference to the texture being rendered. + * @member {PIXI.Texture} + */ + get texture() + { + return this.uniforms.uSampler; + } + set texture(value) + { + this.uniforms.uSampler = value; + } + + /** + * This gets automatically set by the object using this. + * @default 1 + * @member {number} + */ + set alpha(value) + { + if (value === this._alpha) return; + + this._alpha = value; + this._colorDirty = true; + } + get alpha() + { + return this._alpha; + } + + /** + * Multiply tint for the material. + * @member {number} + * @default 0xFFFFFF + */ + set tint(value) + { + if (value === this._tint) return; + + this._tint = value; + this._tintRGB = (value >> 16) + (value & 0xff00) + ((value & 0xff) << 16); + this._colorDirty = true; + } + get tint() + { + return this._tint; + } + + /** + * Gets called automatically by the Mesh. Intended to be overridden for custom + * MeshMaterial objects. + */ + update() + { + if (this._colorDirty) + { + this._colorDirty = false; + const baseTexture = this.texture.baseTexture; + + premultiplyTintToRgba(this._tintRGB, this._alpha, this.uniforms.uColor, baseTexture.premultiplyAlpha); + } + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/packages/mesh/src/MeshGeometry.js b/packages/mesh/src/MeshGeometry.js new file mode 100644 index 0000000..ae6c702 --- /dev/null +++ b/packages/mesh/src/MeshGeometry.js @@ -0,0 +1,61 @@ +import { TYPES } from '@pixi/constants'; +import { Buffer, Geometry } from '@pixi/core'; + +/** + * Standard 2D geometry used in PixiJS. + * + * Geometry can be defined without passing in a style or data if required. + * + * ```js + * const geometry = new PIXI.Geometry(); + * + * geometry.addAttribute('positions', [0, 0, 100, 0, 100, 100, 0, 100], 2); + * geometry.addAttribute('uvs', [0,0,1,0,1,1,0,1], 2); + * geometry.addIndex([0,1,2,1,3,2]); + * + * ``` + * @class + * @memberof PIXI + * @extends PIXI.Geometry + */ +export default class MeshGeometry extends Geometry +{ + /** + * @param {Float32Array|number[]} vertices - Positional data on geometry. + * @param {Float32Array|number[]} uvs - Texture UVs. + * @param {Uint16Array|number[]} index - IndexBuffer + */ + constructor(vertices, uvs, index) + { + super(); + + const verticesBuffer = new Buffer(vertices); + const uvsBuffer = new Buffer(uvs, true); + const indexBuffer = new Buffer(index, true, true); + + this.addAttribute('aVertexPosition', verticesBuffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', uvsBuffer, 2, false, TYPES.FLOAT) + .addIndex(indexBuffer); + + /** + * Dirty flag to limit update calls on Mesh. For example, + * limiting updates on a single Mesh instance with a shared Geometry + * within the render loop. + * @private + * @member {number} + * @default -1 + */ + this._updateId = -1; + } + + /** + * If the vertex position is updated. + * @member {number} + * @readonly + * @private + */ + get vertexDirtyId() + { + return this.buffers[0]._updateID; + } +} diff --git a/packages/mesh/src/MeshMaterial.js b/packages/mesh/src/MeshMaterial.js new file mode 100644 index 0000000..028cf49 --- /dev/null +++ b/packages/mesh/src/MeshMaterial.js @@ -0,0 +1,130 @@ +import { Shader, Program, TextureMatrix } from '@pixi/core'; +import vertex from './shader/mesh.vert'; +import fragment from './shader/mesh.frag'; +import { Matrix } from '@pixi/math'; +import { premultiplyTintToRgba } from '@pixi/utils'; + +/** + * Slightly opinionated default shader for PixiJS 2D objects. + * @class + * @memberof PIXI + * @extends PIXI.Shader + */ +export default class MeshMaterial extends Shader +{ + /** + * @param {PIXI.Texture} uSampler - Texture that material uses to render. + * @param {object} [options] - Additional options + * @param {number} [options.alpha=1] - Default alpha. + * @param {number} [options.tint=0xFFFFFF] - Default tint. + */ + constructor(uSampler, options) + { + const program = Program.from(vertex, fragment); + + const uniforms = { + uSampler, + alpha: 1, + uTextureMatrix: Matrix.IDENTITY, + uColor: new Float32Array([1, 1, 1, 1]), + }; + + super(program, uniforms); + + /** + * Only do update if tint or alpha changes. + * @member {boolean} + * @private + * @default false + */ + this._colorDirty = false; + + /** + * TextureMatrix instance for this Mesh, used to track Texture changes + * + * @member {PIXI.TextureMatrix} + * @readonly + */ + // TODO get this back in! + this.uvMatrix = new TextureMatrix(this.uSampler); + + /** + * `true` if shader can be batch with the renderer's batch system. + * @member {boolean} + * @default true + */ + this.batchable = true; + + // Set defaults + const { tint, alpha } = Object.assign({ + tint: 0xFFFFFF, + alpha: 1, + }, options); + + this.tint = tint; + this.alpha = alpha; + } + + /** + * Reference to the texture being rendered. + * @member {PIXI.Texture} + */ + get texture() + { + return this.uniforms.uSampler; + } + set texture(value) + { + this.uniforms.uSampler = value; + } + + /** + * This gets automatically set by the object using this. + * @default 1 + * @member {number} + */ + set alpha(value) + { + if (value === this._alpha) return; + + this._alpha = value; + this._colorDirty = true; + } + get alpha() + { + return this._alpha; + } + + /** + * Multiply tint for the material. + * @member {number} + * @default 0xFFFFFF + */ + set tint(value) + { + if (value === this._tint) return; + + this._tint = value; + this._tintRGB = (value >> 16) + (value & 0xff00) + ((value & 0xff) << 16); + this._colorDirty = true; + } + get tint() + { + return this._tint; + } + + /** + * Gets called automatically by the Mesh. Intended to be overridden for custom + * MeshMaterial objects. + */ + update() + { + if (this._colorDirty) + { + this._colorDirty = false; + const baseTexture = this.texture.baseTexture; + + premultiplyTintToRgba(this._tintRGB, this._alpha, this.uniforms.uColor, baseTexture.premultiplyAlpha); + } + } +} diff --git a/packages/mesh/src/MeshRenderer.js b/packages/mesh/src/MeshRenderer.js deleted file mode 100644 index d7baf9b..0000000 --- a/packages/mesh/src/MeshRenderer.js +++ /dev/null @@ -1,77 +0,0 @@ -import { ObjectRenderer } from '@pixi/core'; -import { Matrix } from '@pixi/math'; - -/** - * WebGL renderer plugin for tiling sprites - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class MeshRenderer extends ObjectRenderer -{ - /** - * constructor for renderer - * - * @param {Renderer} renderer The renderer this tiling awesomeness works for. - */ - constructor(renderer) - { - super(renderer); - - this.shader = null; - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * renders mesh - * @private - * @param {PIXI.Mesh} mesh mesh instance - */ - render(mesh) - { - // bind the shader.. - - // TODO - // set the shader props.. - // probably only need to set once! - // as its then a reference.. - if (mesh.shader.program.uniformData.translationMatrix) - { - // the transform! - mesh.shader.uniforms.translationMatrix = mesh.transform.worldTransform.toArray(true); - } - if (mesh.shader.uniforms.uTextureMatrix) - { - if (mesh.uploadUvMatrix) - { - mesh.shader.uniforms.uTextureMatrix = mesh.uvMatrix.mapCoord.toArray(true); - } - else - { - mesh.shader.uniforms.uTextureMatrix = Matrix.IDENTITY.toArray(true); - } - } - - // bind and sync uniforms.. - this.renderer.shader.bind(mesh.shader); - - // set state.. - this.renderer.state.setState(mesh.state); - - // bind the geometry... - this.renderer.geometry.bind(mesh.geometry, mesh.shader); - // then render it - this.renderer.geometry.draw(mesh.drawMode, mesh.size, mesh.start, mesh.geometry.instanceCount); - } -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/packages/mesh/src/MeshGeometry.js b/packages/mesh/src/MeshGeometry.js new file mode 100644 index 0000000..ae6c702 --- /dev/null +++ b/packages/mesh/src/MeshGeometry.js @@ -0,0 +1,61 @@ +import { TYPES } from '@pixi/constants'; +import { Buffer, Geometry } from '@pixi/core'; + +/** + * Standard 2D geometry used in PixiJS. + * + * Geometry can be defined without passing in a style or data if required. + * + * ```js + * const geometry = new PIXI.Geometry(); + * + * geometry.addAttribute('positions', [0, 0, 100, 0, 100, 100, 0, 100], 2); + * geometry.addAttribute('uvs', [0,0,1,0,1,1,0,1], 2); + * geometry.addIndex([0,1,2,1,3,2]); + * + * ``` + * @class + * @memberof PIXI + * @extends PIXI.Geometry + */ +export default class MeshGeometry extends Geometry +{ + /** + * @param {Float32Array|number[]} vertices - Positional data on geometry. + * @param {Float32Array|number[]} uvs - Texture UVs. + * @param {Uint16Array|number[]} index - IndexBuffer + */ + constructor(vertices, uvs, index) + { + super(); + + const verticesBuffer = new Buffer(vertices); + const uvsBuffer = new Buffer(uvs, true); + const indexBuffer = new Buffer(index, true, true); + + this.addAttribute('aVertexPosition', verticesBuffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', uvsBuffer, 2, false, TYPES.FLOAT) + .addIndex(indexBuffer); + + /** + * Dirty flag to limit update calls on Mesh. For example, + * limiting updates on a single Mesh instance with a shared Geometry + * within the render loop. + * @private + * @member {number} + * @default -1 + */ + this._updateId = -1; + } + + /** + * If the vertex position is updated. + * @member {number} + * @readonly + * @private + */ + get vertexDirtyId() + { + return this.buffers[0]._updateID; + } +} diff --git a/packages/mesh/src/MeshMaterial.js b/packages/mesh/src/MeshMaterial.js new file mode 100644 index 0000000..028cf49 --- /dev/null +++ b/packages/mesh/src/MeshMaterial.js @@ -0,0 +1,130 @@ +import { Shader, Program, TextureMatrix } from '@pixi/core'; +import vertex from './shader/mesh.vert'; +import fragment from './shader/mesh.frag'; +import { Matrix } from '@pixi/math'; +import { premultiplyTintToRgba } from '@pixi/utils'; + +/** + * Slightly opinionated default shader for PixiJS 2D objects. + * @class + * @memberof PIXI + * @extends PIXI.Shader + */ +export default class MeshMaterial extends Shader +{ + /** + * @param {PIXI.Texture} uSampler - Texture that material uses to render. + * @param {object} [options] - Additional options + * @param {number} [options.alpha=1] - Default alpha. + * @param {number} [options.tint=0xFFFFFF] - Default tint. + */ + constructor(uSampler, options) + { + const program = Program.from(vertex, fragment); + + const uniforms = { + uSampler, + alpha: 1, + uTextureMatrix: Matrix.IDENTITY, + uColor: new Float32Array([1, 1, 1, 1]), + }; + + super(program, uniforms); + + /** + * Only do update if tint or alpha changes. + * @member {boolean} + * @private + * @default false + */ + this._colorDirty = false; + + /** + * TextureMatrix instance for this Mesh, used to track Texture changes + * + * @member {PIXI.TextureMatrix} + * @readonly + */ + // TODO get this back in! + this.uvMatrix = new TextureMatrix(this.uSampler); + + /** + * `true` if shader can be batch with the renderer's batch system. + * @member {boolean} + * @default true + */ + this.batchable = true; + + // Set defaults + const { tint, alpha } = Object.assign({ + tint: 0xFFFFFF, + alpha: 1, + }, options); + + this.tint = tint; + this.alpha = alpha; + } + + /** + * Reference to the texture being rendered. + * @member {PIXI.Texture} + */ + get texture() + { + return this.uniforms.uSampler; + } + set texture(value) + { + this.uniforms.uSampler = value; + } + + /** + * This gets automatically set by the object using this. + * @default 1 + * @member {number} + */ + set alpha(value) + { + if (value === this._alpha) return; + + this._alpha = value; + this._colorDirty = true; + } + get alpha() + { + return this._alpha; + } + + /** + * Multiply tint for the material. + * @member {number} + * @default 0xFFFFFF + */ + set tint(value) + { + if (value === this._tint) return; + + this._tint = value; + this._tintRGB = (value >> 16) + (value & 0xff00) + ((value & 0xff) << 16); + this._colorDirty = true; + } + get tint() + { + return this._tint; + } + + /** + * Gets called automatically by the Mesh. Intended to be overridden for custom + * MeshMaterial objects. + */ + update() + { + if (this._colorDirty) + { + this._colorDirty = false; + const baseTexture = this.texture.baseTexture; + + premultiplyTintToRgba(this._tintRGB, this._alpha, this.uniforms.uColor, baseTexture.premultiplyAlpha); + } + } +} diff --git a/packages/mesh/src/MeshRenderer.js b/packages/mesh/src/MeshRenderer.js deleted file mode 100644 index d7baf9b..0000000 --- a/packages/mesh/src/MeshRenderer.js +++ /dev/null @@ -1,77 +0,0 @@ -import { ObjectRenderer } from '@pixi/core'; -import { Matrix } from '@pixi/math'; - -/** - * WebGL renderer plugin for tiling sprites - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class MeshRenderer extends ObjectRenderer -{ - /** - * constructor for renderer - * - * @param {Renderer} renderer The renderer this tiling awesomeness works for. - */ - constructor(renderer) - { - super(renderer); - - this.shader = null; - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * renders mesh - * @private - * @param {PIXI.Mesh} mesh mesh instance - */ - render(mesh) - { - // bind the shader.. - - // TODO - // set the shader props.. - // probably only need to set once! - // as its then a reference.. - if (mesh.shader.program.uniformData.translationMatrix) - { - // the transform! - mesh.shader.uniforms.translationMatrix = mesh.transform.worldTransform.toArray(true); - } - if (mesh.shader.uniforms.uTextureMatrix) - { - if (mesh.uploadUvMatrix) - { - mesh.shader.uniforms.uTextureMatrix = mesh.uvMatrix.mapCoord.toArray(true); - } - else - { - mesh.shader.uniforms.uTextureMatrix = Matrix.IDENTITY.toArray(true); - } - } - - // bind and sync uniforms.. - this.renderer.shader.bind(mesh.shader); - - // set state.. - this.renderer.state.setState(mesh.state); - - // bind the geometry... - this.renderer.geometry.bind(mesh.geometry, mesh.shader); - // then render it - this.renderer.geometry.draw(mesh.drawMode, mesh.size, mesh.start, mesh.geometry.instanceCount); - } -} diff --git a/packages/mesh/src/index.js b/packages/mesh/src/index.js index e14ab9f..9e6dcd8 100644 --- a/packages/mesh/src/index.js +++ b/packages/mesh/src/index.js @@ -1,2 +1,4 @@ export { default as Mesh } from './Mesh'; -export { default as MeshRenderer } from './MeshRenderer'; +export { default as MeshMaterial } from './MeshMaterial'; +export { default as MeshGeometry } from './MeshGeometry'; + diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/packages/mesh/src/MeshGeometry.js b/packages/mesh/src/MeshGeometry.js new file mode 100644 index 0000000..ae6c702 --- /dev/null +++ b/packages/mesh/src/MeshGeometry.js @@ -0,0 +1,61 @@ +import { TYPES } from '@pixi/constants'; +import { Buffer, Geometry } from '@pixi/core'; + +/** + * Standard 2D geometry used in PixiJS. + * + * Geometry can be defined without passing in a style or data if required. + * + * ```js + * const geometry = new PIXI.Geometry(); + * + * geometry.addAttribute('positions', [0, 0, 100, 0, 100, 100, 0, 100], 2); + * geometry.addAttribute('uvs', [0,0,1,0,1,1,0,1], 2); + * geometry.addIndex([0,1,2,1,3,2]); + * + * ``` + * @class + * @memberof PIXI + * @extends PIXI.Geometry + */ +export default class MeshGeometry extends Geometry +{ + /** + * @param {Float32Array|number[]} vertices - Positional data on geometry. + * @param {Float32Array|number[]} uvs - Texture UVs. + * @param {Uint16Array|number[]} index - IndexBuffer + */ + constructor(vertices, uvs, index) + { + super(); + + const verticesBuffer = new Buffer(vertices); + const uvsBuffer = new Buffer(uvs, true); + const indexBuffer = new Buffer(index, true, true); + + this.addAttribute('aVertexPosition', verticesBuffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', uvsBuffer, 2, false, TYPES.FLOAT) + .addIndex(indexBuffer); + + /** + * Dirty flag to limit update calls on Mesh. For example, + * limiting updates on a single Mesh instance with a shared Geometry + * within the render loop. + * @private + * @member {number} + * @default -1 + */ + this._updateId = -1; + } + + /** + * If the vertex position is updated. + * @member {number} + * @readonly + * @private + */ + get vertexDirtyId() + { + return this.buffers[0]._updateID; + } +} diff --git a/packages/mesh/src/MeshMaterial.js b/packages/mesh/src/MeshMaterial.js new file mode 100644 index 0000000..028cf49 --- /dev/null +++ b/packages/mesh/src/MeshMaterial.js @@ -0,0 +1,130 @@ +import { Shader, Program, TextureMatrix } from '@pixi/core'; +import vertex from './shader/mesh.vert'; +import fragment from './shader/mesh.frag'; +import { Matrix } from '@pixi/math'; +import { premultiplyTintToRgba } from '@pixi/utils'; + +/** + * Slightly opinionated default shader for PixiJS 2D objects. + * @class + * @memberof PIXI + * @extends PIXI.Shader + */ +export default class MeshMaterial extends Shader +{ + /** + * @param {PIXI.Texture} uSampler - Texture that material uses to render. + * @param {object} [options] - Additional options + * @param {number} [options.alpha=1] - Default alpha. + * @param {number} [options.tint=0xFFFFFF] - Default tint. + */ + constructor(uSampler, options) + { + const program = Program.from(vertex, fragment); + + const uniforms = { + uSampler, + alpha: 1, + uTextureMatrix: Matrix.IDENTITY, + uColor: new Float32Array([1, 1, 1, 1]), + }; + + super(program, uniforms); + + /** + * Only do update if tint or alpha changes. + * @member {boolean} + * @private + * @default false + */ + this._colorDirty = false; + + /** + * TextureMatrix instance for this Mesh, used to track Texture changes + * + * @member {PIXI.TextureMatrix} + * @readonly + */ + // TODO get this back in! + this.uvMatrix = new TextureMatrix(this.uSampler); + + /** + * `true` if shader can be batch with the renderer's batch system. + * @member {boolean} + * @default true + */ + this.batchable = true; + + // Set defaults + const { tint, alpha } = Object.assign({ + tint: 0xFFFFFF, + alpha: 1, + }, options); + + this.tint = tint; + this.alpha = alpha; + } + + /** + * Reference to the texture being rendered. + * @member {PIXI.Texture} + */ + get texture() + { + return this.uniforms.uSampler; + } + set texture(value) + { + this.uniforms.uSampler = value; + } + + /** + * This gets automatically set by the object using this. + * @default 1 + * @member {number} + */ + set alpha(value) + { + if (value === this._alpha) return; + + this._alpha = value; + this._colorDirty = true; + } + get alpha() + { + return this._alpha; + } + + /** + * Multiply tint for the material. + * @member {number} + * @default 0xFFFFFF + */ + set tint(value) + { + if (value === this._tint) return; + + this._tint = value; + this._tintRGB = (value >> 16) + (value & 0xff00) + ((value & 0xff) << 16); + this._colorDirty = true; + } + get tint() + { + return this._tint; + } + + /** + * Gets called automatically by the Mesh. Intended to be overridden for custom + * MeshMaterial objects. + */ + update() + { + if (this._colorDirty) + { + this._colorDirty = false; + const baseTexture = this.texture.baseTexture; + + premultiplyTintToRgba(this._tintRGB, this._alpha, this.uniforms.uColor, baseTexture.premultiplyAlpha); + } + } +} diff --git a/packages/mesh/src/MeshRenderer.js b/packages/mesh/src/MeshRenderer.js deleted file mode 100644 index d7baf9b..0000000 --- a/packages/mesh/src/MeshRenderer.js +++ /dev/null @@ -1,77 +0,0 @@ -import { ObjectRenderer } from '@pixi/core'; -import { Matrix } from '@pixi/math'; - -/** - * WebGL renderer plugin for tiling sprites - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class MeshRenderer extends ObjectRenderer -{ - /** - * constructor for renderer - * - * @param {Renderer} renderer The renderer this tiling awesomeness works for. - */ - constructor(renderer) - { - super(renderer); - - this.shader = null; - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * renders mesh - * @private - * @param {PIXI.Mesh} mesh mesh instance - */ - render(mesh) - { - // bind the shader.. - - // TODO - // set the shader props.. - // probably only need to set once! - // as its then a reference.. - if (mesh.shader.program.uniformData.translationMatrix) - { - // the transform! - mesh.shader.uniforms.translationMatrix = mesh.transform.worldTransform.toArray(true); - } - if (mesh.shader.uniforms.uTextureMatrix) - { - if (mesh.uploadUvMatrix) - { - mesh.shader.uniforms.uTextureMatrix = mesh.uvMatrix.mapCoord.toArray(true); - } - else - { - mesh.shader.uniforms.uTextureMatrix = Matrix.IDENTITY.toArray(true); - } - } - - // bind and sync uniforms.. - this.renderer.shader.bind(mesh.shader); - - // set state.. - this.renderer.state.setState(mesh.state); - - // bind the geometry... - this.renderer.geometry.bind(mesh.geometry, mesh.shader); - // then render it - this.renderer.geometry.draw(mesh.drawMode, mesh.size, mesh.start, mesh.geometry.instanceCount); - } -} diff --git a/packages/mesh/src/index.js b/packages/mesh/src/index.js index e14ab9f..9e6dcd8 100644 --- a/packages/mesh/src/index.js +++ b/packages/mesh/src/index.js @@ -1,2 +1,4 @@ export { default as Mesh } from './Mesh'; -export { default as MeshRenderer } from './MeshRenderer'; +export { default as MeshMaterial } from './MeshMaterial'; +export { default as MeshGeometry } from './MeshGeometry'; + diff --git a/packages/mesh/src/shader/mesh.frag b/packages/mesh/src/shader/mesh.frag new file mode 100644 index 0000000..6096983 --- /dev/null +++ b/packages/mesh/src/shader/mesh.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +uniform vec4 uColor; + +uniform sampler2D uSampler; + +void main(void) +{ + gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/packages/mesh/src/MeshGeometry.js b/packages/mesh/src/MeshGeometry.js new file mode 100644 index 0000000..ae6c702 --- /dev/null +++ b/packages/mesh/src/MeshGeometry.js @@ -0,0 +1,61 @@ +import { TYPES } from '@pixi/constants'; +import { Buffer, Geometry } from '@pixi/core'; + +/** + * Standard 2D geometry used in PixiJS. + * + * Geometry can be defined without passing in a style or data if required. + * + * ```js + * const geometry = new PIXI.Geometry(); + * + * geometry.addAttribute('positions', [0, 0, 100, 0, 100, 100, 0, 100], 2); + * geometry.addAttribute('uvs', [0,0,1,0,1,1,0,1], 2); + * geometry.addIndex([0,1,2,1,3,2]); + * + * ``` + * @class + * @memberof PIXI + * @extends PIXI.Geometry + */ +export default class MeshGeometry extends Geometry +{ + /** + * @param {Float32Array|number[]} vertices - Positional data on geometry. + * @param {Float32Array|number[]} uvs - Texture UVs. + * @param {Uint16Array|number[]} index - IndexBuffer + */ + constructor(vertices, uvs, index) + { + super(); + + const verticesBuffer = new Buffer(vertices); + const uvsBuffer = new Buffer(uvs, true); + const indexBuffer = new Buffer(index, true, true); + + this.addAttribute('aVertexPosition', verticesBuffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', uvsBuffer, 2, false, TYPES.FLOAT) + .addIndex(indexBuffer); + + /** + * Dirty flag to limit update calls on Mesh. For example, + * limiting updates on a single Mesh instance with a shared Geometry + * within the render loop. + * @private + * @member {number} + * @default -1 + */ + this._updateId = -1; + } + + /** + * If the vertex position is updated. + * @member {number} + * @readonly + * @private + */ + get vertexDirtyId() + { + return this.buffers[0]._updateID; + } +} diff --git a/packages/mesh/src/MeshMaterial.js b/packages/mesh/src/MeshMaterial.js new file mode 100644 index 0000000..028cf49 --- /dev/null +++ b/packages/mesh/src/MeshMaterial.js @@ -0,0 +1,130 @@ +import { Shader, Program, TextureMatrix } from '@pixi/core'; +import vertex from './shader/mesh.vert'; +import fragment from './shader/mesh.frag'; +import { Matrix } from '@pixi/math'; +import { premultiplyTintToRgba } from '@pixi/utils'; + +/** + * Slightly opinionated default shader for PixiJS 2D objects. + * @class + * @memberof PIXI + * @extends PIXI.Shader + */ +export default class MeshMaterial extends Shader +{ + /** + * @param {PIXI.Texture} uSampler - Texture that material uses to render. + * @param {object} [options] - Additional options + * @param {number} [options.alpha=1] - Default alpha. + * @param {number} [options.tint=0xFFFFFF] - Default tint. + */ + constructor(uSampler, options) + { + const program = Program.from(vertex, fragment); + + const uniforms = { + uSampler, + alpha: 1, + uTextureMatrix: Matrix.IDENTITY, + uColor: new Float32Array([1, 1, 1, 1]), + }; + + super(program, uniforms); + + /** + * Only do update if tint or alpha changes. + * @member {boolean} + * @private + * @default false + */ + this._colorDirty = false; + + /** + * TextureMatrix instance for this Mesh, used to track Texture changes + * + * @member {PIXI.TextureMatrix} + * @readonly + */ + // TODO get this back in! + this.uvMatrix = new TextureMatrix(this.uSampler); + + /** + * `true` if shader can be batch with the renderer's batch system. + * @member {boolean} + * @default true + */ + this.batchable = true; + + // Set defaults + const { tint, alpha } = Object.assign({ + tint: 0xFFFFFF, + alpha: 1, + }, options); + + this.tint = tint; + this.alpha = alpha; + } + + /** + * Reference to the texture being rendered. + * @member {PIXI.Texture} + */ + get texture() + { + return this.uniforms.uSampler; + } + set texture(value) + { + this.uniforms.uSampler = value; + } + + /** + * This gets automatically set by the object using this. + * @default 1 + * @member {number} + */ + set alpha(value) + { + if (value === this._alpha) return; + + this._alpha = value; + this._colorDirty = true; + } + get alpha() + { + return this._alpha; + } + + /** + * Multiply tint for the material. + * @member {number} + * @default 0xFFFFFF + */ + set tint(value) + { + if (value === this._tint) return; + + this._tint = value; + this._tintRGB = (value >> 16) + (value & 0xff00) + ((value & 0xff) << 16); + this._colorDirty = true; + } + get tint() + { + return this._tint; + } + + /** + * Gets called automatically by the Mesh. Intended to be overridden for custom + * MeshMaterial objects. + */ + update() + { + if (this._colorDirty) + { + this._colorDirty = false; + const baseTexture = this.texture.baseTexture; + + premultiplyTintToRgba(this._tintRGB, this._alpha, this.uniforms.uColor, baseTexture.premultiplyAlpha); + } + } +} diff --git a/packages/mesh/src/MeshRenderer.js b/packages/mesh/src/MeshRenderer.js deleted file mode 100644 index d7baf9b..0000000 --- a/packages/mesh/src/MeshRenderer.js +++ /dev/null @@ -1,77 +0,0 @@ -import { ObjectRenderer } from '@pixi/core'; -import { Matrix } from '@pixi/math'; - -/** - * WebGL renderer plugin for tiling sprites - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class MeshRenderer extends ObjectRenderer -{ - /** - * constructor for renderer - * - * @param {Renderer} renderer The renderer this tiling awesomeness works for. - */ - constructor(renderer) - { - super(renderer); - - this.shader = null; - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * renders mesh - * @private - * @param {PIXI.Mesh} mesh mesh instance - */ - render(mesh) - { - // bind the shader.. - - // TODO - // set the shader props.. - // probably only need to set once! - // as its then a reference.. - if (mesh.shader.program.uniformData.translationMatrix) - { - // the transform! - mesh.shader.uniforms.translationMatrix = mesh.transform.worldTransform.toArray(true); - } - if (mesh.shader.uniforms.uTextureMatrix) - { - if (mesh.uploadUvMatrix) - { - mesh.shader.uniforms.uTextureMatrix = mesh.uvMatrix.mapCoord.toArray(true); - } - else - { - mesh.shader.uniforms.uTextureMatrix = Matrix.IDENTITY.toArray(true); - } - } - - // bind and sync uniforms.. - this.renderer.shader.bind(mesh.shader); - - // set state.. - this.renderer.state.setState(mesh.state); - - // bind the geometry... - this.renderer.geometry.bind(mesh.geometry, mesh.shader); - // then render it - this.renderer.geometry.draw(mesh.drawMode, mesh.size, mesh.start, mesh.geometry.instanceCount); - } -} diff --git a/packages/mesh/src/index.js b/packages/mesh/src/index.js index e14ab9f..9e6dcd8 100644 --- a/packages/mesh/src/index.js +++ b/packages/mesh/src/index.js @@ -1,2 +1,4 @@ export { default as Mesh } from './Mesh'; -export { default as MeshRenderer } from './MeshRenderer'; +export { default as MeshMaterial } from './MeshMaterial'; +export { default as MeshGeometry } from './MeshGeometry'; + diff --git a/packages/mesh/src/shader/mesh.frag b/packages/mesh/src/shader/mesh.frag new file mode 100644 index 0000000..6096983 --- /dev/null +++ b/packages/mesh/src/shader/mesh.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +uniform vec4 uColor; + +uniform sampler2D uSampler; + +void main(void) +{ + gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; +} diff --git a/packages/mesh/src/shader/mesh.vert b/packages/mesh/src/shader/mesh.vert new file mode 100644 index 0000000..0526df9 --- /dev/null +++ b/packages/mesh/src/shader/mesh.vert @@ -0,0 +1,15 @@ +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTextureMatrix; + +varying vec2 vTextureCoord; + +void main(void) +{ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/packages/mesh/src/MeshGeometry.js b/packages/mesh/src/MeshGeometry.js new file mode 100644 index 0000000..ae6c702 --- /dev/null +++ b/packages/mesh/src/MeshGeometry.js @@ -0,0 +1,61 @@ +import { TYPES } from '@pixi/constants'; +import { Buffer, Geometry } from '@pixi/core'; + +/** + * Standard 2D geometry used in PixiJS. + * + * Geometry can be defined without passing in a style or data if required. + * + * ```js + * const geometry = new PIXI.Geometry(); + * + * geometry.addAttribute('positions', [0, 0, 100, 0, 100, 100, 0, 100], 2); + * geometry.addAttribute('uvs', [0,0,1,0,1,1,0,1], 2); + * geometry.addIndex([0,1,2,1,3,2]); + * + * ``` + * @class + * @memberof PIXI + * @extends PIXI.Geometry + */ +export default class MeshGeometry extends Geometry +{ + /** + * @param {Float32Array|number[]} vertices - Positional data on geometry. + * @param {Float32Array|number[]} uvs - Texture UVs. + * @param {Uint16Array|number[]} index - IndexBuffer + */ + constructor(vertices, uvs, index) + { + super(); + + const verticesBuffer = new Buffer(vertices); + const uvsBuffer = new Buffer(uvs, true); + const indexBuffer = new Buffer(index, true, true); + + this.addAttribute('aVertexPosition', verticesBuffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', uvsBuffer, 2, false, TYPES.FLOAT) + .addIndex(indexBuffer); + + /** + * Dirty flag to limit update calls on Mesh. For example, + * limiting updates on a single Mesh instance with a shared Geometry + * within the render loop. + * @private + * @member {number} + * @default -1 + */ + this._updateId = -1; + } + + /** + * If the vertex position is updated. + * @member {number} + * @readonly + * @private + */ + get vertexDirtyId() + { + return this.buffers[0]._updateID; + } +} diff --git a/packages/mesh/src/MeshMaterial.js b/packages/mesh/src/MeshMaterial.js new file mode 100644 index 0000000..028cf49 --- /dev/null +++ b/packages/mesh/src/MeshMaterial.js @@ -0,0 +1,130 @@ +import { Shader, Program, TextureMatrix } from '@pixi/core'; +import vertex from './shader/mesh.vert'; +import fragment from './shader/mesh.frag'; +import { Matrix } from '@pixi/math'; +import { premultiplyTintToRgba } from '@pixi/utils'; + +/** + * Slightly opinionated default shader for PixiJS 2D objects. + * @class + * @memberof PIXI + * @extends PIXI.Shader + */ +export default class MeshMaterial extends Shader +{ + /** + * @param {PIXI.Texture} uSampler - Texture that material uses to render. + * @param {object} [options] - Additional options + * @param {number} [options.alpha=1] - Default alpha. + * @param {number} [options.tint=0xFFFFFF] - Default tint. + */ + constructor(uSampler, options) + { + const program = Program.from(vertex, fragment); + + const uniforms = { + uSampler, + alpha: 1, + uTextureMatrix: Matrix.IDENTITY, + uColor: new Float32Array([1, 1, 1, 1]), + }; + + super(program, uniforms); + + /** + * Only do update if tint or alpha changes. + * @member {boolean} + * @private + * @default false + */ + this._colorDirty = false; + + /** + * TextureMatrix instance for this Mesh, used to track Texture changes + * + * @member {PIXI.TextureMatrix} + * @readonly + */ + // TODO get this back in! + this.uvMatrix = new TextureMatrix(this.uSampler); + + /** + * `true` if shader can be batch with the renderer's batch system. + * @member {boolean} + * @default true + */ + this.batchable = true; + + // Set defaults + const { tint, alpha } = Object.assign({ + tint: 0xFFFFFF, + alpha: 1, + }, options); + + this.tint = tint; + this.alpha = alpha; + } + + /** + * Reference to the texture being rendered. + * @member {PIXI.Texture} + */ + get texture() + { + return this.uniforms.uSampler; + } + set texture(value) + { + this.uniforms.uSampler = value; + } + + /** + * This gets automatically set by the object using this. + * @default 1 + * @member {number} + */ + set alpha(value) + { + if (value === this._alpha) return; + + this._alpha = value; + this._colorDirty = true; + } + get alpha() + { + return this._alpha; + } + + /** + * Multiply tint for the material. + * @member {number} + * @default 0xFFFFFF + */ + set tint(value) + { + if (value === this._tint) return; + + this._tint = value; + this._tintRGB = (value >> 16) + (value & 0xff00) + ((value & 0xff) << 16); + this._colorDirty = true; + } + get tint() + { + return this._tint; + } + + /** + * Gets called automatically by the Mesh. Intended to be overridden for custom + * MeshMaterial objects. + */ + update() + { + if (this._colorDirty) + { + this._colorDirty = false; + const baseTexture = this.texture.baseTexture; + + premultiplyTintToRgba(this._tintRGB, this._alpha, this.uniforms.uColor, baseTexture.premultiplyAlpha); + } + } +} diff --git a/packages/mesh/src/MeshRenderer.js b/packages/mesh/src/MeshRenderer.js deleted file mode 100644 index d7baf9b..0000000 --- a/packages/mesh/src/MeshRenderer.js +++ /dev/null @@ -1,77 +0,0 @@ -import { ObjectRenderer } from '@pixi/core'; -import { Matrix } from '@pixi/math'; - -/** - * WebGL renderer plugin for tiling sprites - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class MeshRenderer extends ObjectRenderer -{ - /** - * constructor for renderer - * - * @param {Renderer} renderer The renderer this tiling awesomeness works for. - */ - constructor(renderer) - { - super(renderer); - - this.shader = null; - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * renders mesh - * @private - * @param {PIXI.Mesh} mesh mesh instance - */ - render(mesh) - { - // bind the shader.. - - // TODO - // set the shader props.. - // probably only need to set once! - // as its then a reference.. - if (mesh.shader.program.uniformData.translationMatrix) - { - // the transform! - mesh.shader.uniforms.translationMatrix = mesh.transform.worldTransform.toArray(true); - } - if (mesh.shader.uniforms.uTextureMatrix) - { - if (mesh.uploadUvMatrix) - { - mesh.shader.uniforms.uTextureMatrix = mesh.uvMatrix.mapCoord.toArray(true); - } - else - { - mesh.shader.uniforms.uTextureMatrix = Matrix.IDENTITY.toArray(true); - } - } - - // bind and sync uniforms.. - this.renderer.shader.bind(mesh.shader); - - // set state.. - this.renderer.state.setState(mesh.state); - - // bind the geometry... - this.renderer.geometry.bind(mesh.geometry, mesh.shader); - // then render it - this.renderer.geometry.draw(mesh.drawMode, mesh.size, mesh.start, mesh.geometry.instanceCount); - } -} diff --git a/packages/mesh/src/index.js b/packages/mesh/src/index.js index e14ab9f..9e6dcd8 100644 --- a/packages/mesh/src/index.js +++ b/packages/mesh/src/index.js @@ -1,2 +1,4 @@ export { default as Mesh } from './Mesh'; -export { default as MeshRenderer } from './MeshRenderer'; +export { default as MeshMaterial } from './MeshMaterial'; +export { default as MeshGeometry } from './MeshGeometry'; + diff --git a/packages/mesh/src/shader/mesh.frag b/packages/mesh/src/shader/mesh.frag new file mode 100644 index 0000000..6096983 --- /dev/null +++ b/packages/mesh/src/shader/mesh.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +uniform vec4 uColor; + +uniform sampler2D uSampler; + +void main(void) +{ + gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; +} diff --git a/packages/mesh/src/shader/mesh.vert b/packages/mesh/src/shader/mesh.vert new file mode 100644 index 0000000..0526df9 --- /dev/null +++ b/packages/mesh/src/shader/mesh.vert @@ -0,0 +1,15 @@ +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTextureMatrix; + +varying vec2 vTextureCoord; + +void main(void) +{ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; +} diff --git a/packages/sprite/README.md b/packages/sprite/README.md index 3783526..1972ceb 100644 --- a/packages/sprite/README.md +++ b/packages/sprite/README.md @@ -9,8 +9,7 @@ ## Usage ```js -import { SpriteRenderer } from '@pixi/sprite'; -import { Renderer } from '@pixi/core'; +import { Sprite } from '@pixi/sprite'; -Renderer.registerPlugin('sprite', SpriteRenderer); +const sprite = new Sprite(); ``` \ No newline at end of file diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/packages/mesh/src/MeshGeometry.js b/packages/mesh/src/MeshGeometry.js new file mode 100644 index 0000000..ae6c702 --- /dev/null +++ b/packages/mesh/src/MeshGeometry.js @@ -0,0 +1,61 @@ +import { TYPES } from '@pixi/constants'; +import { Buffer, Geometry } from '@pixi/core'; + +/** + * Standard 2D geometry used in PixiJS. + * + * Geometry can be defined without passing in a style or data if required. + * + * ```js + * const geometry = new PIXI.Geometry(); + * + * geometry.addAttribute('positions', [0, 0, 100, 0, 100, 100, 0, 100], 2); + * geometry.addAttribute('uvs', [0,0,1,0,1,1,0,1], 2); + * geometry.addIndex([0,1,2,1,3,2]); + * + * ``` + * @class + * @memberof PIXI + * @extends PIXI.Geometry + */ +export default class MeshGeometry extends Geometry +{ + /** + * @param {Float32Array|number[]} vertices - Positional data on geometry. + * @param {Float32Array|number[]} uvs - Texture UVs. + * @param {Uint16Array|number[]} index - IndexBuffer + */ + constructor(vertices, uvs, index) + { + super(); + + const verticesBuffer = new Buffer(vertices); + const uvsBuffer = new Buffer(uvs, true); + const indexBuffer = new Buffer(index, true, true); + + this.addAttribute('aVertexPosition', verticesBuffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', uvsBuffer, 2, false, TYPES.FLOAT) + .addIndex(indexBuffer); + + /** + * Dirty flag to limit update calls on Mesh. For example, + * limiting updates on a single Mesh instance with a shared Geometry + * within the render loop. + * @private + * @member {number} + * @default -1 + */ + this._updateId = -1; + } + + /** + * If the vertex position is updated. + * @member {number} + * @readonly + * @private + */ + get vertexDirtyId() + { + return this.buffers[0]._updateID; + } +} diff --git a/packages/mesh/src/MeshMaterial.js b/packages/mesh/src/MeshMaterial.js new file mode 100644 index 0000000..028cf49 --- /dev/null +++ b/packages/mesh/src/MeshMaterial.js @@ -0,0 +1,130 @@ +import { Shader, Program, TextureMatrix } from '@pixi/core'; +import vertex from './shader/mesh.vert'; +import fragment from './shader/mesh.frag'; +import { Matrix } from '@pixi/math'; +import { premultiplyTintToRgba } from '@pixi/utils'; + +/** + * Slightly opinionated default shader for PixiJS 2D objects. + * @class + * @memberof PIXI + * @extends PIXI.Shader + */ +export default class MeshMaterial extends Shader +{ + /** + * @param {PIXI.Texture} uSampler - Texture that material uses to render. + * @param {object} [options] - Additional options + * @param {number} [options.alpha=1] - Default alpha. + * @param {number} [options.tint=0xFFFFFF] - Default tint. + */ + constructor(uSampler, options) + { + const program = Program.from(vertex, fragment); + + const uniforms = { + uSampler, + alpha: 1, + uTextureMatrix: Matrix.IDENTITY, + uColor: new Float32Array([1, 1, 1, 1]), + }; + + super(program, uniforms); + + /** + * Only do update if tint or alpha changes. + * @member {boolean} + * @private + * @default false + */ + this._colorDirty = false; + + /** + * TextureMatrix instance for this Mesh, used to track Texture changes + * + * @member {PIXI.TextureMatrix} + * @readonly + */ + // TODO get this back in! + this.uvMatrix = new TextureMatrix(this.uSampler); + + /** + * `true` if shader can be batch with the renderer's batch system. + * @member {boolean} + * @default true + */ + this.batchable = true; + + // Set defaults + const { tint, alpha } = Object.assign({ + tint: 0xFFFFFF, + alpha: 1, + }, options); + + this.tint = tint; + this.alpha = alpha; + } + + /** + * Reference to the texture being rendered. + * @member {PIXI.Texture} + */ + get texture() + { + return this.uniforms.uSampler; + } + set texture(value) + { + this.uniforms.uSampler = value; + } + + /** + * This gets automatically set by the object using this. + * @default 1 + * @member {number} + */ + set alpha(value) + { + if (value === this._alpha) return; + + this._alpha = value; + this._colorDirty = true; + } + get alpha() + { + return this._alpha; + } + + /** + * Multiply tint for the material. + * @member {number} + * @default 0xFFFFFF + */ + set tint(value) + { + if (value === this._tint) return; + + this._tint = value; + this._tintRGB = (value >> 16) + (value & 0xff00) + ((value & 0xff) << 16); + this._colorDirty = true; + } + get tint() + { + return this._tint; + } + + /** + * Gets called automatically by the Mesh. Intended to be overridden for custom + * MeshMaterial objects. + */ + update() + { + if (this._colorDirty) + { + this._colorDirty = false; + const baseTexture = this.texture.baseTexture; + + premultiplyTintToRgba(this._tintRGB, this._alpha, this.uniforms.uColor, baseTexture.premultiplyAlpha); + } + } +} diff --git a/packages/mesh/src/MeshRenderer.js b/packages/mesh/src/MeshRenderer.js deleted file mode 100644 index d7baf9b..0000000 --- a/packages/mesh/src/MeshRenderer.js +++ /dev/null @@ -1,77 +0,0 @@ -import { ObjectRenderer } from '@pixi/core'; -import { Matrix } from '@pixi/math'; - -/** - * WebGL renderer plugin for tiling sprites - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class MeshRenderer extends ObjectRenderer -{ - /** - * constructor for renderer - * - * @param {Renderer} renderer The renderer this tiling awesomeness works for. - */ - constructor(renderer) - { - super(renderer); - - this.shader = null; - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * renders mesh - * @private - * @param {PIXI.Mesh} mesh mesh instance - */ - render(mesh) - { - // bind the shader.. - - // TODO - // set the shader props.. - // probably only need to set once! - // as its then a reference.. - if (mesh.shader.program.uniformData.translationMatrix) - { - // the transform! - mesh.shader.uniforms.translationMatrix = mesh.transform.worldTransform.toArray(true); - } - if (mesh.shader.uniforms.uTextureMatrix) - { - if (mesh.uploadUvMatrix) - { - mesh.shader.uniforms.uTextureMatrix = mesh.uvMatrix.mapCoord.toArray(true); - } - else - { - mesh.shader.uniforms.uTextureMatrix = Matrix.IDENTITY.toArray(true); - } - } - - // bind and sync uniforms.. - this.renderer.shader.bind(mesh.shader); - - // set state.. - this.renderer.state.setState(mesh.state); - - // bind the geometry... - this.renderer.geometry.bind(mesh.geometry, mesh.shader); - // then render it - this.renderer.geometry.draw(mesh.drawMode, mesh.size, mesh.start, mesh.geometry.instanceCount); - } -} diff --git a/packages/mesh/src/index.js b/packages/mesh/src/index.js index e14ab9f..9e6dcd8 100644 --- a/packages/mesh/src/index.js +++ b/packages/mesh/src/index.js @@ -1,2 +1,4 @@ export { default as Mesh } from './Mesh'; -export { default as MeshRenderer } from './MeshRenderer'; +export { default as MeshMaterial } from './MeshMaterial'; +export { default as MeshGeometry } from './MeshGeometry'; + diff --git a/packages/mesh/src/shader/mesh.frag b/packages/mesh/src/shader/mesh.frag new file mode 100644 index 0000000..6096983 --- /dev/null +++ b/packages/mesh/src/shader/mesh.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +uniform vec4 uColor; + +uniform sampler2D uSampler; + +void main(void) +{ + gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; +} diff --git a/packages/mesh/src/shader/mesh.vert b/packages/mesh/src/shader/mesh.vert new file mode 100644 index 0000000..0526df9 --- /dev/null +++ b/packages/mesh/src/shader/mesh.vert @@ -0,0 +1,15 @@ +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTextureMatrix; + +varying vec2 vTextureCoord; + +void main(void) +{ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; +} diff --git a/packages/sprite/README.md b/packages/sprite/README.md index 3783526..1972ceb 100644 --- a/packages/sprite/README.md +++ b/packages/sprite/README.md @@ -9,8 +9,7 @@ ## Usage ```js -import { SpriteRenderer } from '@pixi/sprite'; -import { Renderer } from '@pixi/core'; +import { Sprite } from '@pixi/sprite'; -Renderer.registerPlugin('sprite', SpriteRenderer); +const sprite = new Sprite(); ``` \ No newline at end of file diff --git a/packages/sprite/src/BatchBuffer.js b/packages/sprite/src/BatchBuffer.js deleted file mode 100644 index cbc4c72..0000000 --- a/packages/sprite/src/BatchBuffer.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.positions = null; - this.uvs = null; - this.colors = null; - } -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/packages/mesh/src/MeshGeometry.js b/packages/mesh/src/MeshGeometry.js new file mode 100644 index 0000000..ae6c702 --- /dev/null +++ b/packages/mesh/src/MeshGeometry.js @@ -0,0 +1,61 @@ +import { TYPES } from '@pixi/constants'; +import { Buffer, Geometry } from '@pixi/core'; + +/** + * Standard 2D geometry used in PixiJS. + * + * Geometry can be defined without passing in a style or data if required. + * + * ```js + * const geometry = new PIXI.Geometry(); + * + * geometry.addAttribute('positions', [0, 0, 100, 0, 100, 100, 0, 100], 2); + * geometry.addAttribute('uvs', [0,0,1,0,1,1,0,1], 2); + * geometry.addIndex([0,1,2,1,3,2]); + * + * ``` + * @class + * @memberof PIXI + * @extends PIXI.Geometry + */ +export default class MeshGeometry extends Geometry +{ + /** + * @param {Float32Array|number[]} vertices - Positional data on geometry. + * @param {Float32Array|number[]} uvs - Texture UVs. + * @param {Uint16Array|number[]} index - IndexBuffer + */ + constructor(vertices, uvs, index) + { + super(); + + const verticesBuffer = new Buffer(vertices); + const uvsBuffer = new Buffer(uvs, true); + const indexBuffer = new Buffer(index, true, true); + + this.addAttribute('aVertexPosition', verticesBuffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', uvsBuffer, 2, false, TYPES.FLOAT) + .addIndex(indexBuffer); + + /** + * Dirty flag to limit update calls on Mesh. For example, + * limiting updates on a single Mesh instance with a shared Geometry + * within the render loop. + * @private + * @member {number} + * @default -1 + */ + this._updateId = -1; + } + + /** + * If the vertex position is updated. + * @member {number} + * @readonly + * @private + */ + get vertexDirtyId() + { + return this.buffers[0]._updateID; + } +} diff --git a/packages/mesh/src/MeshMaterial.js b/packages/mesh/src/MeshMaterial.js new file mode 100644 index 0000000..028cf49 --- /dev/null +++ b/packages/mesh/src/MeshMaterial.js @@ -0,0 +1,130 @@ +import { Shader, Program, TextureMatrix } from '@pixi/core'; +import vertex from './shader/mesh.vert'; +import fragment from './shader/mesh.frag'; +import { Matrix } from '@pixi/math'; +import { premultiplyTintToRgba } from '@pixi/utils'; + +/** + * Slightly opinionated default shader for PixiJS 2D objects. + * @class + * @memberof PIXI + * @extends PIXI.Shader + */ +export default class MeshMaterial extends Shader +{ + /** + * @param {PIXI.Texture} uSampler - Texture that material uses to render. + * @param {object} [options] - Additional options + * @param {number} [options.alpha=1] - Default alpha. + * @param {number} [options.tint=0xFFFFFF] - Default tint. + */ + constructor(uSampler, options) + { + const program = Program.from(vertex, fragment); + + const uniforms = { + uSampler, + alpha: 1, + uTextureMatrix: Matrix.IDENTITY, + uColor: new Float32Array([1, 1, 1, 1]), + }; + + super(program, uniforms); + + /** + * Only do update if tint or alpha changes. + * @member {boolean} + * @private + * @default false + */ + this._colorDirty = false; + + /** + * TextureMatrix instance for this Mesh, used to track Texture changes + * + * @member {PIXI.TextureMatrix} + * @readonly + */ + // TODO get this back in! + this.uvMatrix = new TextureMatrix(this.uSampler); + + /** + * `true` if shader can be batch with the renderer's batch system. + * @member {boolean} + * @default true + */ + this.batchable = true; + + // Set defaults + const { tint, alpha } = Object.assign({ + tint: 0xFFFFFF, + alpha: 1, + }, options); + + this.tint = tint; + this.alpha = alpha; + } + + /** + * Reference to the texture being rendered. + * @member {PIXI.Texture} + */ + get texture() + { + return this.uniforms.uSampler; + } + set texture(value) + { + this.uniforms.uSampler = value; + } + + /** + * This gets automatically set by the object using this. + * @default 1 + * @member {number} + */ + set alpha(value) + { + if (value === this._alpha) return; + + this._alpha = value; + this._colorDirty = true; + } + get alpha() + { + return this._alpha; + } + + /** + * Multiply tint for the material. + * @member {number} + * @default 0xFFFFFF + */ + set tint(value) + { + if (value === this._tint) return; + + this._tint = value; + this._tintRGB = (value >> 16) + (value & 0xff00) + ((value & 0xff) << 16); + this._colorDirty = true; + } + get tint() + { + return this._tint; + } + + /** + * Gets called automatically by the Mesh. Intended to be overridden for custom + * MeshMaterial objects. + */ + update() + { + if (this._colorDirty) + { + this._colorDirty = false; + const baseTexture = this.texture.baseTexture; + + premultiplyTintToRgba(this._tintRGB, this._alpha, this.uniforms.uColor, baseTexture.premultiplyAlpha); + } + } +} diff --git a/packages/mesh/src/MeshRenderer.js b/packages/mesh/src/MeshRenderer.js deleted file mode 100644 index d7baf9b..0000000 --- a/packages/mesh/src/MeshRenderer.js +++ /dev/null @@ -1,77 +0,0 @@ -import { ObjectRenderer } from '@pixi/core'; -import { Matrix } from '@pixi/math'; - -/** - * WebGL renderer plugin for tiling sprites - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class MeshRenderer extends ObjectRenderer -{ - /** - * constructor for renderer - * - * @param {Renderer} renderer The renderer this tiling awesomeness works for. - */ - constructor(renderer) - { - super(renderer); - - this.shader = null; - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * renders mesh - * @private - * @param {PIXI.Mesh} mesh mesh instance - */ - render(mesh) - { - // bind the shader.. - - // TODO - // set the shader props.. - // probably only need to set once! - // as its then a reference.. - if (mesh.shader.program.uniformData.translationMatrix) - { - // the transform! - mesh.shader.uniforms.translationMatrix = mesh.transform.worldTransform.toArray(true); - } - if (mesh.shader.uniforms.uTextureMatrix) - { - if (mesh.uploadUvMatrix) - { - mesh.shader.uniforms.uTextureMatrix = mesh.uvMatrix.mapCoord.toArray(true); - } - else - { - mesh.shader.uniforms.uTextureMatrix = Matrix.IDENTITY.toArray(true); - } - } - - // bind and sync uniforms.. - this.renderer.shader.bind(mesh.shader); - - // set state.. - this.renderer.state.setState(mesh.state); - - // bind the geometry... - this.renderer.geometry.bind(mesh.geometry, mesh.shader); - // then render it - this.renderer.geometry.draw(mesh.drawMode, mesh.size, mesh.start, mesh.geometry.instanceCount); - } -} diff --git a/packages/mesh/src/index.js b/packages/mesh/src/index.js index e14ab9f..9e6dcd8 100644 --- a/packages/mesh/src/index.js +++ b/packages/mesh/src/index.js @@ -1,2 +1,4 @@ export { default as Mesh } from './Mesh'; -export { default as MeshRenderer } from './MeshRenderer'; +export { default as MeshMaterial } from './MeshMaterial'; +export { default as MeshGeometry } from './MeshGeometry'; + diff --git a/packages/mesh/src/shader/mesh.frag b/packages/mesh/src/shader/mesh.frag new file mode 100644 index 0000000..6096983 --- /dev/null +++ b/packages/mesh/src/shader/mesh.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +uniform vec4 uColor; + +uniform sampler2D uSampler; + +void main(void) +{ + gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; +} diff --git a/packages/mesh/src/shader/mesh.vert b/packages/mesh/src/shader/mesh.vert new file mode 100644 index 0000000..0526df9 --- /dev/null +++ b/packages/mesh/src/shader/mesh.vert @@ -0,0 +1,15 @@ +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTextureMatrix; + +varying vec2 vTextureCoord; + +void main(void) +{ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; +} diff --git a/packages/sprite/README.md b/packages/sprite/README.md index 3783526..1972ceb 100644 --- a/packages/sprite/README.md +++ b/packages/sprite/README.md @@ -9,8 +9,7 @@ ## Usage ```js -import { SpriteRenderer } from '@pixi/sprite'; -import { Renderer } from '@pixi/core'; +import { Sprite } from '@pixi/sprite'; -Renderer.registerPlugin('sprite', SpriteRenderer); +const sprite = new Sprite(); ``` \ No newline at end of file diff --git a/packages/sprite/src/BatchBuffer.js b/packages/sprite/src/BatchBuffer.js deleted file mode 100644 index cbc4c72..0000000 --- a/packages/sprite/src/BatchBuffer.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.positions = null; - this.uvs = null; - this.colors = null; - } -} diff --git a/packages/sprite/src/Sprite.js b/packages/sprite/src/Sprite.js index 91546cf..7b66e6e 100644 --- a/packages/sprite/src/Sprite.js +++ b/packages/sprite/src/Sprite.js @@ -5,10 +5,11 @@ import { Container } from '@pixi/display'; const tempPoint = new Point(); +const indices = new Uint16Array([0, 1, 2, 0, 2, 3]); /** * The Sprite object is the base for all textured objects that are rendered to the screen - * +* * A sprite can be created directly from an image like this: * * ```js @@ -119,6 +120,8 @@ */ this.cachedTint = 0xFFFFFF; + this.uvs = null; + // call texture setter this.texture = texture || Texture.EMPTY; @@ -144,6 +147,12 @@ this._transformTrimmedID = -1; this._textureTrimmedID = -1; + // Batchable stuff.. + // TODO could make this a mixin? + this.indices = indices; + this.size = 4; + this.start = 0; + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. @@ -151,7 +160,12 @@ * @member {string} * @default 'sprite' */ - this.pluginName = 'sprite'; + this.pluginName = 'batch'; + + /** + * used to fast check if a sprite is.. a sprite! + */ + this.isSprite = true; } /** @@ -165,6 +179,7 @@ this._textureTrimmedID = -1; this.cachedTint = 0xFFFFFF; + this.uvs = this._texture._uvs.uvsFloat32; // so if _width is 0 then width was not set.. if (this._width) { diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/packages/mesh/src/MeshGeometry.js b/packages/mesh/src/MeshGeometry.js new file mode 100644 index 0000000..ae6c702 --- /dev/null +++ b/packages/mesh/src/MeshGeometry.js @@ -0,0 +1,61 @@ +import { TYPES } from '@pixi/constants'; +import { Buffer, Geometry } from '@pixi/core'; + +/** + * Standard 2D geometry used in PixiJS. + * + * Geometry can be defined without passing in a style or data if required. + * + * ```js + * const geometry = new PIXI.Geometry(); + * + * geometry.addAttribute('positions', [0, 0, 100, 0, 100, 100, 0, 100], 2); + * geometry.addAttribute('uvs', [0,0,1,0,1,1,0,1], 2); + * geometry.addIndex([0,1,2,1,3,2]); + * + * ``` + * @class + * @memberof PIXI + * @extends PIXI.Geometry + */ +export default class MeshGeometry extends Geometry +{ + /** + * @param {Float32Array|number[]} vertices - Positional data on geometry. + * @param {Float32Array|number[]} uvs - Texture UVs. + * @param {Uint16Array|number[]} index - IndexBuffer + */ + constructor(vertices, uvs, index) + { + super(); + + const verticesBuffer = new Buffer(vertices); + const uvsBuffer = new Buffer(uvs, true); + const indexBuffer = new Buffer(index, true, true); + + this.addAttribute('aVertexPosition', verticesBuffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', uvsBuffer, 2, false, TYPES.FLOAT) + .addIndex(indexBuffer); + + /** + * Dirty flag to limit update calls on Mesh. For example, + * limiting updates on a single Mesh instance with a shared Geometry + * within the render loop. + * @private + * @member {number} + * @default -1 + */ + this._updateId = -1; + } + + /** + * If the vertex position is updated. + * @member {number} + * @readonly + * @private + */ + get vertexDirtyId() + { + return this.buffers[0]._updateID; + } +} diff --git a/packages/mesh/src/MeshMaterial.js b/packages/mesh/src/MeshMaterial.js new file mode 100644 index 0000000..028cf49 --- /dev/null +++ b/packages/mesh/src/MeshMaterial.js @@ -0,0 +1,130 @@ +import { Shader, Program, TextureMatrix } from '@pixi/core'; +import vertex from './shader/mesh.vert'; +import fragment from './shader/mesh.frag'; +import { Matrix } from '@pixi/math'; +import { premultiplyTintToRgba } from '@pixi/utils'; + +/** + * Slightly opinionated default shader for PixiJS 2D objects. + * @class + * @memberof PIXI + * @extends PIXI.Shader + */ +export default class MeshMaterial extends Shader +{ + /** + * @param {PIXI.Texture} uSampler - Texture that material uses to render. + * @param {object} [options] - Additional options + * @param {number} [options.alpha=1] - Default alpha. + * @param {number} [options.tint=0xFFFFFF] - Default tint. + */ + constructor(uSampler, options) + { + const program = Program.from(vertex, fragment); + + const uniforms = { + uSampler, + alpha: 1, + uTextureMatrix: Matrix.IDENTITY, + uColor: new Float32Array([1, 1, 1, 1]), + }; + + super(program, uniforms); + + /** + * Only do update if tint or alpha changes. + * @member {boolean} + * @private + * @default false + */ + this._colorDirty = false; + + /** + * TextureMatrix instance for this Mesh, used to track Texture changes + * + * @member {PIXI.TextureMatrix} + * @readonly + */ + // TODO get this back in! + this.uvMatrix = new TextureMatrix(this.uSampler); + + /** + * `true` if shader can be batch with the renderer's batch system. + * @member {boolean} + * @default true + */ + this.batchable = true; + + // Set defaults + const { tint, alpha } = Object.assign({ + tint: 0xFFFFFF, + alpha: 1, + }, options); + + this.tint = tint; + this.alpha = alpha; + } + + /** + * Reference to the texture being rendered. + * @member {PIXI.Texture} + */ + get texture() + { + return this.uniforms.uSampler; + } + set texture(value) + { + this.uniforms.uSampler = value; + } + + /** + * This gets automatically set by the object using this. + * @default 1 + * @member {number} + */ + set alpha(value) + { + if (value === this._alpha) return; + + this._alpha = value; + this._colorDirty = true; + } + get alpha() + { + return this._alpha; + } + + /** + * Multiply tint for the material. + * @member {number} + * @default 0xFFFFFF + */ + set tint(value) + { + if (value === this._tint) return; + + this._tint = value; + this._tintRGB = (value >> 16) + (value & 0xff00) + ((value & 0xff) << 16); + this._colorDirty = true; + } + get tint() + { + return this._tint; + } + + /** + * Gets called automatically by the Mesh. Intended to be overridden for custom + * MeshMaterial objects. + */ + update() + { + if (this._colorDirty) + { + this._colorDirty = false; + const baseTexture = this.texture.baseTexture; + + premultiplyTintToRgba(this._tintRGB, this._alpha, this.uniforms.uColor, baseTexture.premultiplyAlpha); + } + } +} diff --git a/packages/mesh/src/MeshRenderer.js b/packages/mesh/src/MeshRenderer.js deleted file mode 100644 index d7baf9b..0000000 --- a/packages/mesh/src/MeshRenderer.js +++ /dev/null @@ -1,77 +0,0 @@ -import { ObjectRenderer } from '@pixi/core'; -import { Matrix } from '@pixi/math'; - -/** - * WebGL renderer plugin for tiling sprites - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class MeshRenderer extends ObjectRenderer -{ - /** - * constructor for renderer - * - * @param {Renderer} renderer The renderer this tiling awesomeness works for. - */ - constructor(renderer) - { - super(renderer); - - this.shader = null; - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * renders mesh - * @private - * @param {PIXI.Mesh} mesh mesh instance - */ - render(mesh) - { - // bind the shader.. - - // TODO - // set the shader props.. - // probably only need to set once! - // as its then a reference.. - if (mesh.shader.program.uniformData.translationMatrix) - { - // the transform! - mesh.shader.uniforms.translationMatrix = mesh.transform.worldTransform.toArray(true); - } - if (mesh.shader.uniforms.uTextureMatrix) - { - if (mesh.uploadUvMatrix) - { - mesh.shader.uniforms.uTextureMatrix = mesh.uvMatrix.mapCoord.toArray(true); - } - else - { - mesh.shader.uniforms.uTextureMatrix = Matrix.IDENTITY.toArray(true); - } - } - - // bind and sync uniforms.. - this.renderer.shader.bind(mesh.shader); - - // set state.. - this.renderer.state.setState(mesh.state); - - // bind the geometry... - this.renderer.geometry.bind(mesh.geometry, mesh.shader); - // then render it - this.renderer.geometry.draw(mesh.drawMode, mesh.size, mesh.start, mesh.geometry.instanceCount); - } -} diff --git a/packages/mesh/src/index.js b/packages/mesh/src/index.js index e14ab9f..9e6dcd8 100644 --- a/packages/mesh/src/index.js +++ b/packages/mesh/src/index.js @@ -1,2 +1,4 @@ export { default as Mesh } from './Mesh'; -export { default as MeshRenderer } from './MeshRenderer'; +export { default as MeshMaterial } from './MeshMaterial'; +export { default as MeshGeometry } from './MeshGeometry'; + diff --git a/packages/mesh/src/shader/mesh.frag b/packages/mesh/src/shader/mesh.frag new file mode 100644 index 0000000..6096983 --- /dev/null +++ b/packages/mesh/src/shader/mesh.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +uniform vec4 uColor; + +uniform sampler2D uSampler; + +void main(void) +{ + gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; +} diff --git a/packages/mesh/src/shader/mesh.vert b/packages/mesh/src/shader/mesh.vert new file mode 100644 index 0000000..0526df9 --- /dev/null +++ b/packages/mesh/src/shader/mesh.vert @@ -0,0 +1,15 @@ +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTextureMatrix; + +varying vec2 vTextureCoord; + +void main(void) +{ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; +} diff --git a/packages/sprite/README.md b/packages/sprite/README.md index 3783526..1972ceb 100644 --- a/packages/sprite/README.md +++ b/packages/sprite/README.md @@ -9,8 +9,7 @@ ## Usage ```js -import { SpriteRenderer } from '@pixi/sprite'; -import { Renderer } from '@pixi/core'; +import { Sprite } from '@pixi/sprite'; -Renderer.registerPlugin('sprite', SpriteRenderer); +const sprite = new Sprite(); ``` \ No newline at end of file diff --git a/packages/sprite/src/BatchBuffer.js b/packages/sprite/src/BatchBuffer.js deleted file mode 100644 index cbc4c72..0000000 --- a/packages/sprite/src/BatchBuffer.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.positions = null; - this.uvs = null; - this.colors = null; - } -} diff --git a/packages/sprite/src/Sprite.js b/packages/sprite/src/Sprite.js index 91546cf..7b66e6e 100644 --- a/packages/sprite/src/Sprite.js +++ b/packages/sprite/src/Sprite.js @@ -5,10 +5,11 @@ import { Container } from '@pixi/display'; const tempPoint = new Point(); +const indices = new Uint16Array([0, 1, 2, 0, 2, 3]); /** * The Sprite object is the base for all textured objects that are rendered to the screen - * +* * A sprite can be created directly from an image like this: * * ```js @@ -119,6 +120,8 @@ */ this.cachedTint = 0xFFFFFF; + this.uvs = null; + // call texture setter this.texture = texture || Texture.EMPTY; @@ -144,6 +147,12 @@ this._transformTrimmedID = -1; this._textureTrimmedID = -1; + // Batchable stuff.. + // TODO could make this a mixin? + this.indices = indices; + this.size = 4; + this.start = 0; + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. @@ -151,7 +160,12 @@ * @member {string} * @default 'sprite' */ - this.pluginName = 'sprite'; + this.pluginName = 'batch'; + + /** + * used to fast check if a sprite is.. a sprite! + */ + this.isSprite = true; } /** @@ -165,6 +179,7 @@ this._textureTrimmedID = -1; this.cachedTint = 0xFFFFFF; + this.uvs = this._texture._uvs.uvsFloat32; // so if _width is 0 then width was not set.. if (this._width) { diff --git a/packages/sprite/src/SpriteRenderer.js b/packages/sprite/src/SpriteRenderer.js deleted file mode 100644 index 136cdcc..0000000 --- a/packages/sprite/src/SpriteRenderer.js +++ /dev/null @@ -1,463 +0,0 @@ -import { Geometry, - Buffer, - ObjectRenderer, - checkMaxIfStatementsInShader } from '@pixi/core'; -import { settings } from '@pixi/settings'; -import { createIndicesForQuads, premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; -import bitTwiddle from 'bit-twiddle'; -import BatchBuffer from './BatchBuffer'; -import generateMultiTextureShader from './generateMultiTextureShader'; -import { ENV } from '@pixi/constants'; - -let TICK = 0; -// const TEXTURE_TICK = 0; - -/** - * Renderer dedicated to drawing and batching sprites. - * - * @class - * @private - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class SpriteRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. - */ - constructor(renderer) - { - super(renderer); - - /** - * Number of values sent in the vertex buffer. - * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 - * - * @member {number} - */ - this.vertSize = 5; - - /** - * The size of the vertex information in bytes. - * - * @member {number} - */ - this.vertByteSize = this.vertSize * 4; - - /** - * The number of images in the SpriteRenderer before it flushes. - * - * @member {number} - */ - this.size = settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop - - // the total number of bytes in our batch - // let numVerts = this.size * 4 * this.vertByteSize; - - this.buffers = []; - for (let i = 1; i <= bitTwiddle.nextPow2(this.size); i *= 2) - { - this.buffers.push(new BatchBuffer(i * 4 * this.vertByteSize)); - } - - /** - * Holds the indices of the geometry (quads) to draw - * - * @member {Uint16Array} - */ - this.indices = createIndicesForQuads(this.size); - this.indexBuffer = new Buffer(this.indices, true, true); - - /** - * The default shaders that is used if a sprite doesn't have a more specific one. - * there is a shader for each number of textures that can be rendered. - * These shaders will also be generated on the fly as required. - * @member {PIXI.Shader[]} - */ - this.shader = null; - - this.currentIndex = 0; - this.groups = []; - - for (let k = 0; k < this.size; k++) - { - this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; - } - - this.sprites = []; - - this.vertexBuffers = []; - this.vaos = []; - - this.vaoMax = 2; - this.vertexCount = 0; - - this.renderer.on('prerender', this.onPrerender, this); - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - const gl = this.renderer.gl; - - if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) - { - this.MAX_TEXTURES = 1; - } - else - { - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); - } - - // generate generateMultiTextureProgram, may be a better move? - this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); - - // we use the second shader as the first one depending on your browser may omit aTextureId - // as it is not used by the shader so is optimized out. - for (let i = 0; i < this.vaoMax; i++) - { - const buffer = new Buffer(null, false); - - /* eslint-disable max-len */ - this.vaos[i] = new Geometry() - .addAttribute('aVertexPosition', buffer, 2, false, gl.FLOAT) - .addAttribute('aTextureCoord', buffer, 2, true, gl.UNSIGNED_SHORT) - .addAttribute('aColor', buffer, 4, true, gl.UNSIGNED_BYTE) - .addAttribute('aTextureId', buffer, 1, true, gl.FLOAT) - .addIndex(this.indexBuffer); - /* eslint-enable max-len */ - - this.vertexBuffers[i] = buffer; - } - } - - /** - * Called before the renderer starts rendering. - * - */ - onPrerender() - { - this.vertexCount = 0; - } - - /** - * Renders the sprite object. - * - * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch - */ - render(sprite) - { - // TODO set blend modes.. - // check texture.. - if (this.currentIndex >= this.size) - { - this.flush(); - } - - // get the uvs for the texture - - // if the uvs have not updated then no point rendering just yet! - if (!sprite._texture._uvs) - { - return; - } - - // push a texture. - // increment the batchsize - this.sprites[this.currentIndex++] = sprite; - } - - /** - * Renders the content and empties the current batch. - * - */ - flush() - { - if (this.currentIndex === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - - const np2 = bitTwiddle.nextPow2(this.currentIndex); - const log2 = bitTwiddle.log2(np2); - const buffer = this.buffers[log2]; - - const sprites = this.sprites; - const groups = this.groups; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let nextTexture; - let currentTexture; - let groupCount = 1; - let textureId = 0; - let textureCount = 0; - let currentGroup = groups[0]; - let vertexData; - let uvs; - let blendMode = premultiplyBlendMode[ - sprites[0]._texture.baseTexture.premultiplyAlpha ? 1 : 0][sprites[0].blendMode]; - - currentGroup.textureCount = 0; - currentGroup.start = 0; - currentGroup.blend = blendMode; - - TICK++; - - let i; - - for (i = 0; i < this.currentIndex; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - const sprite = sprites[i]; - - nextTexture = sprite._texture.baseTexture; - textureId = nextTexture._id; - - const spriteBlendMode = premultiplyBlendMode[Number(nextTexture.premultiplyAlpha)][sprite.blendMode]; - - if (blendMode !== spriteBlendMode) - { - blendMode = spriteBlendMode; - - // force the batch to break! - currentTexture = null; - textureCount = MAX_TEXTURES; - TICK++; - } - - if (currentTexture !== nextTexture) - { - currentTexture = nextTexture; - - if (nextTexture._enabled !== TICK) - { - if (textureCount === MAX_TEXTURES) - { - TICK++; - - textureCount = 0; - - currentGroup.size = i - currentGroup.start; - - currentGroup = groups[groupCount++]; - currentGroup.textureCount = 0; - currentGroup.blend = blendMode; - currentGroup.start = i; - } - - nextTexture.touched = touch; - nextTexture._enabled = TICK; - nextTexture._id = textureCount; - - currentGroup.textures[currentGroup.textureCount++] = nextTexture; - textureCount++; - } - } - - vertexData = sprite.vertexData; - - // TODO this sum does not need to be set each frame.. - uvs = sprite._texture._uvs.uvsUint32; - textureId = nextTexture._id; - - if (this.renderer.roundPixels) - { - const resolution = this.renderer.resolution; - - // xy - float32View[index] = ((vertexData[0] * resolution) | 0) / resolution; - float32View[index + 1] = ((vertexData[1] * resolution) | 0) / resolution; - - // xy - float32View[index + 5] = ((vertexData[2] * resolution) | 0) / resolution; - float32View[index + 6] = ((vertexData[3] * resolution) | 0) / resolution; - - // xy - float32View[index + 10] = ((vertexData[4] * resolution) | 0) / resolution; - float32View[index + 11] = ((vertexData[5] * resolution) | 0) / resolution; - - // xy - float32View[index + 15] = ((vertexData[6] * resolution) | 0) / resolution; - float32View[index + 16] = ((vertexData[7] * resolution) | 0) / resolution; - } - else - { - // xy - float32View[index] = vertexData[0]; - float32View[index + 1] = vertexData[1]; - - // xy - float32View[index + 5] = vertexData[2]; - float32View[index + 6] = vertexData[3]; - - // xy - float32View[index + 10] = vertexData[4]; - float32View[index + 11] = vertexData[5]; - - // xy - float32View[index + 15] = vertexData[6]; - float32View[index + 16] = vertexData[7]; - } - - uint32View[index + 2] = uvs[0]; - uint32View[index + 7] = uvs[1]; - uint32View[index + 12] = uvs[2]; - uint32View[index + 17] = uvs[3]; - /* eslint-disable max-len */ - const alpha = Math.min(sprite.worldAlpha, 1.0); - const argb = alpha < 1.0 && nextTexture.premultiplyAlpha ? premultiplyTint(sprite._tintRGB, alpha) - : sprite._tintRGB + (alpha * 255 << 24); - - uint32View[index + 3] = uint32View[index + 8] = uint32View[index + 13] = uint32View[index + 18] = argb; - - float32View[index + 4] = float32View[index + 9] = float32View[index + 14] = float32View[index + 19] = textureId; - /* eslint-enable max-len */ - - index += 20; - } - - currentGroup.size = i - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this.vaoMax <= this.vertexCount) - { - this.vaoMax++; - - const buffer = new Buffer(null, false); - - /* eslint-disable max-len */ - this.vaos[this.vertexCount] = new Geometry() - .addAttribute('aVertexPosition', buffer, 2, false, gl.FLOAT) - .addAttribute('aTextureCoord', buffer, 2, true, gl.UNSIGNED_SHORT) - .addAttribute('aColor', buffer, 4, true, gl.UNSIGNED_BYTE) - .addAttribute('aTextureId', buffer, 1, true, gl.FLOAT) - .addIndex(this.indexBuffer); - /* eslint-enable max-len */ - - this.vertexBuffers[this.vertexCount] = buffer; - } - - this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); - this.renderer.geometry.bind(this.vaos[this.vertexCount]); - - this.vertexCount++; - } - else - { - // lets use the faster option, always use buffer number 0 - this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); - - this.renderer.geometry.updateBuffers(); - } - - // / render the groups.. - for (i = 0; i < groupCount; i++) - { - const group = groups[i]; - const groupTextureCount = group.textureCount; - - for (let j = 0; j < groupTextureCount; j++) - { - this.renderer.texture.bind(group.textures[j], j); - } - - // set the blend mode.. - this.renderer.state.setBlendMode(group.blend); - - gl.drawElements(gl.TRIANGLES, group.size * 6, gl.UNSIGNED_SHORT, group.start * 6 * 2); - } - - // reset elements for the next flush - this.currentIndex = 0; - } - - /** - * Starts a new sprite batch. - */ - start() - { - this.renderer.shader.bind(this.shader); - - if (settings.CAN_UPLOAD_SAME_BUFFER) - { - // bind buffer #0, we don't need others - this.renderer.geometry.bind(this.vaos[this.vertexCount]); - } - } - - /** - * Stops and flushes the current batch. - * - */ - stop() - { - this.flush(); - } - - /** - * Destroys the SpriteRenderer. - * - */ - destroy() - { - for (let i = 0; i < this.vaoMax; i++) - { - if (this.vertexBuffers[i]) - { - this.vertexBuffers[i].destroy(); - } - if (this.vaos[i]) - { - this.vaos[i].destroy(); - } - } - - if (this.indexBuffer) - { - this.indexBuffer.destroy(); - } - - this.renderer.off('prerender', this.onPrerender, this); - - if (this.shader) - { - this.shader.destroy(); - this.shader = null; - } - - this.vertexBuffers = null; - this.vaos = null; - this.indexBuffer = null; - this.indices = null; - - this.sprites = null; - - for (let i = 0; i < this.buffers.length; ++i) - { - this.buffers[i].destroy(); - } - - super.destroy(); - } -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/packages/mesh/src/MeshGeometry.js b/packages/mesh/src/MeshGeometry.js new file mode 100644 index 0000000..ae6c702 --- /dev/null +++ b/packages/mesh/src/MeshGeometry.js @@ -0,0 +1,61 @@ +import { TYPES } from '@pixi/constants'; +import { Buffer, Geometry } from '@pixi/core'; + +/** + * Standard 2D geometry used in PixiJS. + * + * Geometry can be defined without passing in a style or data if required. + * + * ```js + * const geometry = new PIXI.Geometry(); + * + * geometry.addAttribute('positions', [0, 0, 100, 0, 100, 100, 0, 100], 2); + * geometry.addAttribute('uvs', [0,0,1,0,1,1,0,1], 2); + * geometry.addIndex([0,1,2,1,3,2]); + * + * ``` + * @class + * @memberof PIXI + * @extends PIXI.Geometry + */ +export default class MeshGeometry extends Geometry +{ + /** + * @param {Float32Array|number[]} vertices - Positional data on geometry. + * @param {Float32Array|number[]} uvs - Texture UVs. + * @param {Uint16Array|number[]} index - IndexBuffer + */ + constructor(vertices, uvs, index) + { + super(); + + const verticesBuffer = new Buffer(vertices); + const uvsBuffer = new Buffer(uvs, true); + const indexBuffer = new Buffer(index, true, true); + + this.addAttribute('aVertexPosition', verticesBuffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', uvsBuffer, 2, false, TYPES.FLOAT) + .addIndex(indexBuffer); + + /** + * Dirty flag to limit update calls on Mesh. For example, + * limiting updates on a single Mesh instance with a shared Geometry + * within the render loop. + * @private + * @member {number} + * @default -1 + */ + this._updateId = -1; + } + + /** + * If the vertex position is updated. + * @member {number} + * @readonly + * @private + */ + get vertexDirtyId() + { + return this.buffers[0]._updateID; + } +} diff --git a/packages/mesh/src/MeshMaterial.js b/packages/mesh/src/MeshMaterial.js new file mode 100644 index 0000000..028cf49 --- /dev/null +++ b/packages/mesh/src/MeshMaterial.js @@ -0,0 +1,130 @@ +import { Shader, Program, TextureMatrix } from '@pixi/core'; +import vertex from './shader/mesh.vert'; +import fragment from './shader/mesh.frag'; +import { Matrix } from '@pixi/math'; +import { premultiplyTintToRgba } from '@pixi/utils'; + +/** + * Slightly opinionated default shader for PixiJS 2D objects. + * @class + * @memberof PIXI + * @extends PIXI.Shader + */ +export default class MeshMaterial extends Shader +{ + /** + * @param {PIXI.Texture} uSampler - Texture that material uses to render. + * @param {object} [options] - Additional options + * @param {number} [options.alpha=1] - Default alpha. + * @param {number} [options.tint=0xFFFFFF] - Default tint. + */ + constructor(uSampler, options) + { + const program = Program.from(vertex, fragment); + + const uniforms = { + uSampler, + alpha: 1, + uTextureMatrix: Matrix.IDENTITY, + uColor: new Float32Array([1, 1, 1, 1]), + }; + + super(program, uniforms); + + /** + * Only do update if tint or alpha changes. + * @member {boolean} + * @private + * @default false + */ + this._colorDirty = false; + + /** + * TextureMatrix instance for this Mesh, used to track Texture changes + * + * @member {PIXI.TextureMatrix} + * @readonly + */ + // TODO get this back in! + this.uvMatrix = new TextureMatrix(this.uSampler); + + /** + * `true` if shader can be batch with the renderer's batch system. + * @member {boolean} + * @default true + */ + this.batchable = true; + + // Set defaults + const { tint, alpha } = Object.assign({ + tint: 0xFFFFFF, + alpha: 1, + }, options); + + this.tint = tint; + this.alpha = alpha; + } + + /** + * Reference to the texture being rendered. + * @member {PIXI.Texture} + */ + get texture() + { + return this.uniforms.uSampler; + } + set texture(value) + { + this.uniforms.uSampler = value; + } + + /** + * This gets automatically set by the object using this. + * @default 1 + * @member {number} + */ + set alpha(value) + { + if (value === this._alpha) return; + + this._alpha = value; + this._colorDirty = true; + } + get alpha() + { + return this._alpha; + } + + /** + * Multiply tint for the material. + * @member {number} + * @default 0xFFFFFF + */ + set tint(value) + { + if (value === this._tint) return; + + this._tint = value; + this._tintRGB = (value >> 16) + (value & 0xff00) + ((value & 0xff) << 16); + this._colorDirty = true; + } + get tint() + { + return this._tint; + } + + /** + * Gets called automatically by the Mesh. Intended to be overridden for custom + * MeshMaterial objects. + */ + update() + { + if (this._colorDirty) + { + this._colorDirty = false; + const baseTexture = this.texture.baseTexture; + + premultiplyTintToRgba(this._tintRGB, this._alpha, this.uniforms.uColor, baseTexture.premultiplyAlpha); + } + } +} diff --git a/packages/mesh/src/MeshRenderer.js b/packages/mesh/src/MeshRenderer.js deleted file mode 100644 index d7baf9b..0000000 --- a/packages/mesh/src/MeshRenderer.js +++ /dev/null @@ -1,77 +0,0 @@ -import { ObjectRenderer } from '@pixi/core'; -import { Matrix } from '@pixi/math'; - -/** - * WebGL renderer plugin for tiling sprites - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class MeshRenderer extends ObjectRenderer -{ - /** - * constructor for renderer - * - * @param {Renderer} renderer The renderer this tiling awesomeness works for. - */ - constructor(renderer) - { - super(renderer); - - this.shader = null; - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * renders mesh - * @private - * @param {PIXI.Mesh} mesh mesh instance - */ - render(mesh) - { - // bind the shader.. - - // TODO - // set the shader props.. - // probably only need to set once! - // as its then a reference.. - if (mesh.shader.program.uniformData.translationMatrix) - { - // the transform! - mesh.shader.uniforms.translationMatrix = mesh.transform.worldTransform.toArray(true); - } - if (mesh.shader.uniforms.uTextureMatrix) - { - if (mesh.uploadUvMatrix) - { - mesh.shader.uniforms.uTextureMatrix = mesh.uvMatrix.mapCoord.toArray(true); - } - else - { - mesh.shader.uniforms.uTextureMatrix = Matrix.IDENTITY.toArray(true); - } - } - - // bind and sync uniforms.. - this.renderer.shader.bind(mesh.shader); - - // set state.. - this.renderer.state.setState(mesh.state); - - // bind the geometry... - this.renderer.geometry.bind(mesh.geometry, mesh.shader); - // then render it - this.renderer.geometry.draw(mesh.drawMode, mesh.size, mesh.start, mesh.geometry.instanceCount); - } -} diff --git a/packages/mesh/src/index.js b/packages/mesh/src/index.js index e14ab9f..9e6dcd8 100644 --- a/packages/mesh/src/index.js +++ b/packages/mesh/src/index.js @@ -1,2 +1,4 @@ export { default as Mesh } from './Mesh'; -export { default as MeshRenderer } from './MeshRenderer'; +export { default as MeshMaterial } from './MeshMaterial'; +export { default as MeshGeometry } from './MeshGeometry'; + diff --git a/packages/mesh/src/shader/mesh.frag b/packages/mesh/src/shader/mesh.frag new file mode 100644 index 0000000..6096983 --- /dev/null +++ b/packages/mesh/src/shader/mesh.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +uniform vec4 uColor; + +uniform sampler2D uSampler; + +void main(void) +{ + gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; +} diff --git a/packages/mesh/src/shader/mesh.vert b/packages/mesh/src/shader/mesh.vert new file mode 100644 index 0000000..0526df9 --- /dev/null +++ b/packages/mesh/src/shader/mesh.vert @@ -0,0 +1,15 @@ +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTextureMatrix; + +varying vec2 vTextureCoord; + +void main(void) +{ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; +} diff --git a/packages/sprite/README.md b/packages/sprite/README.md index 3783526..1972ceb 100644 --- a/packages/sprite/README.md +++ b/packages/sprite/README.md @@ -9,8 +9,7 @@ ## Usage ```js -import { SpriteRenderer } from '@pixi/sprite'; -import { Renderer } from '@pixi/core'; +import { Sprite } from '@pixi/sprite'; -Renderer.registerPlugin('sprite', SpriteRenderer); +const sprite = new Sprite(); ``` \ No newline at end of file diff --git a/packages/sprite/src/BatchBuffer.js b/packages/sprite/src/BatchBuffer.js deleted file mode 100644 index cbc4c72..0000000 --- a/packages/sprite/src/BatchBuffer.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.positions = null; - this.uvs = null; - this.colors = null; - } -} diff --git a/packages/sprite/src/Sprite.js b/packages/sprite/src/Sprite.js index 91546cf..7b66e6e 100644 --- a/packages/sprite/src/Sprite.js +++ b/packages/sprite/src/Sprite.js @@ -5,10 +5,11 @@ import { Container } from '@pixi/display'; const tempPoint = new Point(); +const indices = new Uint16Array([0, 1, 2, 0, 2, 3]); /** * The Sprite object is the base for all textured objects that are rendered to the screen - * +* * A sprite can be created directly from an image like this: * * ```js @@ -119,6 +120,8 @@ */ this.cachedTint = 0xFFFFFF; + this.uvs = null; + // call texture setter this.texture = texture || Texture.EMPTY; @@ -144,6 +147,12 @@ this._transformTrimmedID = -1; this._textureTrimmedID = -1; + // Batchable stuff.. + // TODO could make this a mixin? + this.indices = indices; + this.size = 4; + this.start = 0; + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. @@ -151,7 +160,12 @@ * @member {string} * @default 'sprite' */ - this.pluginName = 'sprite'; + this.pluginName = 'batch'; + + /** + * used to fast check if a sprite is.. a sprite! + */ + this.isSprite = true; } /** @@ -165,6 +179,7 @@ this._textureTrimmedID = -1; this.cachedTint = 0xFFFFFF; + this.uvs = this._texture._uvs.uvsFloat32; // so if _width is 0 then width was not set.. if (this._width) { diff --git a/packages/sprite/src/SpriteRenderer.js b/packages/sprite/src/SpriteRenderer.js deleted file mode 100644 index 136cdcc..0000000 --- a/packages/sprite/src/SpriteRenderer.js +++ /dev/null @@ -1,463 +0,0 @@ -import { Geometry, - Buffer, - ObjectRenderer, - checkMaxIfStatementsInShader } from '@pixi/core'; -import { settings } from '@pixi/settings'; -import { createIndicesForQuads, premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; -import bitTwiddle from 'bit-twiddle'; -import BatchBuffer from './BatchBuffer'; -import generateMultiTextureShader from './generateMultiTextureShader'; -import { ENV } from '@pixi/constants'; - -let TICK = 0; -// const TEXTURE_TICK = 0; - -/** - * Renderer dedicated to drawing and batching sprites. - * - * @class - * @private - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class SpriteRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. - */ - constructor(renderer) - { - super(renderer); - - /** - * Number of values sent in the vertex buffer. - * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 - * - * @member {number} - */ - this.vertSize = 5; - - /** - * The size of the vertex information in bytes. - * - * @member {number} - */ - this.vertByteSize = this.vertSize * 4; - - /** - * The number of images in the SpriteRenderer before it flushes. - * - * @member {number} - */ - this.size = settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop - - // the total number of bytes in our batch - // let numVerts = this.size * 4 * this.vertByteSize; - - this.buffers = []; - for (let i = 1; i <= bitTwiddle.nextPow2(this.size); i *= 2) - { - this.buffers.push(new BatchBuffer(i * 4 * this.vertByteSize)); - } - - /** - * Holds the indices of the geometry (quads) to draw - * - * @member {Uint16Array} - */ - this.indices = createIndicesForQuads(this.size); - this.indexBuffer = new Buffer(this.indices, true, true); - - /** - * The default shaders that is used if a sprite doesn't have a more specific one. - * there is a shader for each number of textures that can be rendered. - * These shaders will also be generated on the fly as required. - * @member {PIXI.Shader[]} - */ - this.shader = null; - - this.currentIndex = 0; - this.groups = []; - - for (let k = 0; k < this.size; k++) - { - this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; - } - - this.sprites = []; - - this.vertexBuffers = []; - this.vaos = []; - - this.vaoMax = 2; - this.vertexCount = 0; - - this.renderer.on('prerender', this.onPrerender, this); - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - const gl = this.renderer.gl; - - if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) - { - this.MAX_TEXTURES = 1; - } - else - { - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); - } - - // generate generateMultiTextureProgram, may be a better move? - this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); - - // we use the second shader as the first one depending on your browser may omit aTextureId - // as it is not used by the shader so is optimized out. - for (let i = 0; i < this.vaoMax; i++) - { - const buffer = new Buffer(null, false); - - /* eslint-disable max-len */ - this.vaos[i] = new Geometry() - .addAttribute('aVertexPosition', buffer, 2, false, gl.FLOAT) - .addAttribute('aTextureCoord', buffer, 2, true, gl.UNSIGNED_SHORT) - .addAttribute('aColor', buffer, 4, true, gl.UNSIGNED_BYTE) - .addAttribute('aTextureId', buffer, 1, true, gl.FLOAT) - .addIndex(this.indexBuffer); - /* eslint-enable max-len */ - - this.vertexBuffers[i] = buffer; - } - } - - /** - * Called before the renderer starts rendering. - * - */ - onPrerender() - { - this.vertexCount = 0; - } - - /** - * Renders the sprite object. - * - * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch - */ - render(sprite) - { - // TODO set blend modes.. - // check texture.. - if (this.currentIndex >= this.size) - { - this.flush(); - } - - // get the uvs for the texture - - // if the uvs have not updated then no point rendering just yet! - if (!sprite._texture._uvs) - { - return; - } - - // push a texture. - // increment the batchsize - this.sprites[this.currentIndex++] = sprite; - } - - /** - * Renders the content and empties the current batch. - * - */ - flush() - { - if (this.currentIndex === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - - const np2 = bitTwiddle.nextPow2(this.currentIndex); - const log2 = bitTwiddle.log2(np2); - const buffer = this.buffers[log2]; - - const sprites = this.sprites; - const groups = this.groups; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let nextTexture; - let currentTexture; - let groupCount = 1; - let textureId = 0; - let textureCount = 0; - let currentGroup = groups[0]; - let vertexData; - let uvs; - let blendMode = premultiplyBlendMode[ - sprites[0]._texture.baseTexture.premultiplyAlpha ? 1 : 0][sprites[0].blendMode]; - - currentGroup.textureCount = 0; - currentGroup.start = 0; - currentGroup.blend = blendMode; - - TICK++; - - let i; - - for (i = 0; i < this.currentIndex; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - const sprite = sprites[i]; - - nextTexture = sprite._texture.baseTexture; - textureId = nextTexture._id; - - const spriteBlendMode = premultiplyBlendMode[Number(nextTexture.premultiplyAlpha)][sprite.blendMode]; - - if (blendMode !== spriteBlendMode) - { - blendMode = spriteBlendMode; - - // force the batch to break! - currentTexture = null; - textureCount = MAX_TEXTURES; - TICK++; - } - - if (currentTexture !== nextTexture) - { - currentTexture = nextTexture; - - if (nextTexture._enabled !== TICK) - { - if (textureCount === MAX_TEXTURES) - { - TICK++; - - textureCount = 0; - - currentGroup.size = i - currentGroup.start; - - currentGroup = groups[groupCount++]; - currentGroup.textureCount = 0; - currentGroup.blend = blendMode; - currentGroup.start = i; - } - - nextTexture.touched = touch; - nextTexture._enabled = TICK; - nextTexture._id = textureCount; - - currentGroup.textures[currentGroup.textureCount++] = nextTexture; - textureCount++; - } - } - - vertexData = sprite.vertexData; - - // TODO this sum does not need to be set each frame.. - uvs = sprite._texture._uvs.uvsUint32; - textureId = nextTexture._id; - - if (this.renderer.roundPixels) - { - const resolution = this.renderer.resolution; - - // xy - float32View[index] = ((vertexData[0] * resolution) | 0) / resolution; - float32View[index + 1] = ((vertexData[1] * resolution) | 0) / resolution; - - // xy - float32View[index + 5] = ((vertexData[2] * resolution) | 0) / resolution; - float32View[index + 6] = ((vertexData[3] * resolution) | 0) / resolution; - - // xy - float32View[index + 10] = ((vertexData[4] * resolution) | 0) / resolution; - float32View[index + 11] = ((vertexData[5] * resolution) | 0) / resolution; - - // xy - float32View[index + 15] = ((vertexData[6] * resolution) | 0) / resolution; - float32View[index + 16] = ((vertexData[7] * resolution) | 0) / resolution; - } - else - { - // xy - float32View[index] = vertexData[0]; - float32View[index + 1] = vertexData[1]; - - // xy - float32View[index + 5] = vertexData[2]; - float32View[index + 6] = vertexData[3]; - - // xy - float32View[index + 10] = vertexData[4]; - float32View[index + 11] = vertexData[5]; - - // xy - float32View[index + 15] = vertexData[6]; - float32View[index + 16] = vertexData[7]; - } - - uint32View[index + 2] = uvs[0]; - uint32View[index + 7] = uvs[1]; - uint32View[index + 12] = uvs[2]; - uint32View[index + 17] = uvs[3]; - /* eslint-disable max-len */ - const alpha = Math.min(sprite.worldAlpha, 1.0); - const argb = alpha < 1.0 && nextTexture.premultiplyAlpha ? premultiplyTint(sprite._tintRGB, alpha) - : sprite._tintRGB + (alpha * 255 << 24); - - uint32View[index + 3] = uint32View[index + 8] = uint32View[index + 13] = uint32View[index + 18] = argb; - - float32View[index + 4] = float32View[index + 9] = float32View[index + 14] = float32View[index + 19] = textureId; - /* eslint-enable max-len */ - - index += 20; - } - - currentGroup.size = i - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this.vaoMax <= this.vertexCount) - { - this.vaoMax++; - - const buffer = new Buffer(null, false); - - /* eslint-disable max-len */ - this.vaos[this.vertexCount] = new Geometry() - .addAttribute('aVertexPosition', buffer, 2, false, gl.FLOAT) - .addAttribute('aTextureCoord', buffer, 2, true, gl.UNSIGNED_SHORT) - .addAttribute('aColor', buffer, 4, true, gl.UNSIGNED_BYTE) - .addAttribute('aTextureId', buffer, 1, true, gl.FLOAT) - .addIndex(this.indexBuffer); - /* eslint-enable max-len */ - - this.vertexBuffers[this.vertexCount] = buffer; - } - - this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); - this.renderer.geometry.bind(this.vaos[this.vertexCount]); - - this.vertexCount++; - } - else - { - // lets use the faster option, always use buffer number 0 - this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); - - this.renderer.geometry.updateBuffers(); - } - - // / render the groups.. - for (i = 0; i < groupCount; i++) - { - const group = groups[i]; - const groupTextureCount = group.textureCount; - - for (let j = 0; j < groupTextureCount; j++) - { - this.renderer.texture.bind(group.textures[j], j); - } - - // set the blend mode.. - this.renderer.state.setBlendMode(group.blend); - - gl.drawElements(gl.TRIANGLES, group.size * 6, gl.UNSIGNED_SHORT, group.start * 6 * 2); - } - - // reset elements for the next flush - this.currentIndex = 0; - } - - /** - * Starts a new sprite batch. - */ - start() - { - this.renderer.shader.bind(this.shader); - - if (settings.CAN_UPLOAD_SAME_BUFFER) - { - // bind buffer #0, we don't need others - this.renderer.geometry.bind(this.vaos[this.vertexCount]); - } - } - - /** - * Stops and flushes the current batch. - * - */ - stop() - { - this.flush(); - } - - /** - * Destroys the SpriteRenderer. - * - */ - destroy() - { - for (let i = 0; i < this.vaoMax; i++) - { - if (this.vertexBuffers[i]) - { - this.vertexBuffers[i].destroy(); - } - if (this.vaos[i]) - { - this.vaos[i].destroy(); - } - } - - if (this.indexBuffer) - { - this.indexBuffer.destroy(); - } - - this.renderer.off('prerender', this.onPrerender, this); - - if (this.shader) - { - this.shader.destroy(); - this.shader = null; - } - - this.vertexBuffers = null; - this.vaos = null; - this.indexBuffer = null; - this.indices = null; - - this.sprites = null; - - for (let i = 0; i < this.buffers.length; ++i) - { - this.buffers[i].destroy(); - } - - super.destroy(); - } -} diff --git a/packages/sprite/src/generateMultiTextureShader.js b/packages/sprite/src/generateMultiTextureShader.js deleted file mode 100644 index f2e27be..0000000 --- a/packages/sprite/src/generateMultiTextureShader.js +++ /dev/null @@ -1,69 +0,0 @@ -import { Shader, UniformGroup } from '@pixi/core'; -import vertex from './texture.vert'; - -const fragTemplate = [ - 'varying vec2 vTextureCoord;', - 'varying vec4 vColor;', - 'varying float vTextureId;', - 'uniform sampler2D uSamplers[%count%];', - - 'void main(void){', - 'vec4 color;', - 'float textureId = floor(vTextureId+0.5);', - '%forloop%', - 'gl_FragColor = color * vColor;', - '}', -].join('\n'); - -export default function generateMultiTextureShader(gl, maxTextures) -{ - const sampleValues = new Int32Array(maxTextures); - - for (let i = 0; i < maxTextures; i++) - { - sampleValues[i] = i; - } - - const uniforms = { - default: UniformGroup.from({ uSamplers: sampleValues }, true), - }; - - let fragmentSrc = fragTemplate; - - fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); - fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); - - const shader = Shader.from(vertex, fragmentSrc, uniforms); - - return shader; -} - -function generateSampleSrc(maxTextures) -{ - let src = ''; - - src += '\n'; - src += '\n'; - - for (let i = 0; i < maxTextures; i++) - { - if (i > 0) - { - src += '\nelse '; - } - - if (i < maxTextures - 1) - { - src += `if(textureId == ${i}.0)`; - } - - src += '\n{'; - src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; - src += '\n}'; - } - - src += '\n'; - src += '\n'; - - return src; -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/packages/mesh/src/MeshGeometry.js b/packages/mesh/src/MeshGeometry.js new file mode 100644 index 0000000..ae6c702 --- /dev/null +++ b/packages/mesh/src/MeshGeometry.js @@ -0,0 +1,61 @@ +import { TYPES } from '@pixi/constants'; +import { Buffer, Geometry } from '@pixi/core'; + +/** + * Standard 2D geometry used in PixiJS. + * + * Geometry can be defined without passing in a style or data if required. + * + * ```js + * const geometry = new PIXI.Geometry(); + * + * geometry.addAttribute('positions', [0, 0, 100, 0, 100, 100, 0, 100], 2); + * geometry.addAttribute('uvs', [0,0,1,0,1,1,0,1], 2); + * geometry.addIndex([0,1,2,1,3,2]); + * + * ``` + * @class + * @memberof PIXI + * @extends PIXI.Geometry + */ +export default class MeshGeometry extends Geometry +{ + /** + * @param {Float32Array|number[]} vertices - Positional data on geometry. + * @param {Float32Array|number[]} uvs - Texture UVs. + * @param {Uint16Array|number[]} index - IndexBuffer + */ + constructor(vertices, uvs, index) + { + super(); + + const verticesBuffer = new Buffer(vertices); + const uvsBuffer = new Buffer(uvs, true); + const indexBuffer = new Buffer(index, true, true); + + this.addAttribute('aVertexPosition', verticesBuffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', uvsBuffer, 2, false, TYPES.FLOAT) + .addIndex(indexBuffer); + + /** + * Dirty flag to limit update calls on Mesh. For example, + * limiting updates on a single Mesh instance with a shared Geometry + * within the render loop. + * @private + * @member {number} + * @default -1 + */ + this._updateId = -1; + } + + /** + * If the vertex position is updated. + * @member {number} + * @readonly + * @private + */ + get vertexDirtyId() + { + return this.buffers[0]._updateID; + } +} diff --git a/packages/mesh/src/MeshMaterial.js b/packages/mesh/src/MeshMaterial.js new file mode 100644 index 0000000..028cf49 --- /dev/null +++ b/packages/mesh/src/MeshMaterial.js @@ -0,0 +1,130 @@ +import { Shader, Program, TextureMatrix } from '@pixi/core'; +import vertex from './shader/mesh.vert'; +import fragment from './shader/mesh.frag'; +import { Matrix } from '@pixi/math'; +import { premultiplyTintToRgba } from '@pixi/utils'; + +/** + * Slightly opinionated default shader for PixiJS 2D objects. + * @class + * @memberof PIXI + * @extends PIXI.Shader + */ +export default class MeshMaterial extends Shader +{ + /** + * @param {PIXI.Texture} uSampler - Texture that material uses to render. + * @param {object} [options] - Additional options + * @param {number} [options.alpha=1] - Default alpha. + * @param {number} [options.tint=0xFFFFFF] - Default tint. + */ + constructor(uSampler, options) + { + const program = Program.from(vertex, fragment); + + const uniforms = { + uSampler, + alpha: 1, + uTextureMatrix: Matrix.IDENTITY, + uColor: new Float32Array([1, 1, 1, 1]), + }; + + super(program, uniforms); + + /** + * Only do update if tint or alpha changes. + * @member {boolean} + * @private + * @default false + */ + this._colorDirty = false; + + /** + * TextureMatrix instance for this Mesh, used to track Texture changes + * + * @member {PIXI.TextureMatrix} + * @readonly + */ + // TODO get this back in! + this.uvMatrix = new TextureMatrix(this.uSampler); + + /** + * `true` if shader can be batch with the renderer's batch system. + * @member {boolean} + * @default true + */ + this.batchable = true; + + // Set defaults + const { tint, alpha } = Object.assign({ + tint: 0xFFFFFF, + alpha: 1, + }, options); + + this.tint = tint; + this.alpha = alpha; + } + + /** + * Reference to the texture being rendered. + * @member {PIXI.Texture} + */ + get texture() + { + return this.uniforms.uSampler; + } + set texture(value) + { + this.uniforms.uSampler = value; + } + + /** + * This gets automatically set by the object using this. + * @default 1 + * @member {number} + */ + set alpha(value) + { + if (value === this._alpha) return; + + this._alpha = value; + this._colorDirty = true; + } + get alpha() + { + return this._alpha; + } + + /** + * Multiply tint for the material. + * @member {number} + * @default 0xFFFFFF + */ + set tint(value) + { + if (value === this._tint) return; + + this._tint = value; + this._tintRGB = (value >> 16) + (value & 0xff00) + ((value & 0xff) << 16); + this._colorDirty = true; + } + get tint() + { + return this._tint; + } + + /** + * Gets called automatically by the Mesh. Intended to be overridden for custom + * MeshMaterial objects. + */ + update() + { + if (this._colorDirty) + { + this._colorDirty = false; + const baseTexture = this.texture.baseTexture; + + premultiplyTintToRgba(this._tintRGB, this._alpha, this.uniforms.uColor, baseTexture.premultiplyAlpha); + } + } +} diff --git a/packages/mesh/src/MeshRenderer.js b/packages/mesh/src/MeshRenderer.js deleted file mode 100644 index d7baf9b..0000000 --- a/packages/mesh/src/MeshRenderer.js +++ /dev/null @@ -1,77 +0,0 @@ -import { ObjectRenderer } from '@pixi/core'; -import { Matrix } from '@pixi/math'; - -/** - * WebGL renderer plugin for tiling sprites - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class MeshRenderer extends ObjectRenderer -{ - /** - * constructor for renderer - * - * @param {Renderer} renderer The renderer this tiling awesomeness works for. - */ - constructor(renderer) - { - super(renderer); - - this.shader = null; - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * renders mesh - * @private - * @param {PIXI.Mesh} mesh mesh instance - */ - render(mesh) - { - // bind the shader.. - - // TODO - // set the shader props.. - // probably only need to set once! - // as its then a reference.. - if (mesh.shader.program.uniformData.translationMatrix) - { - // the transform! - mesh.shader.uniforms.translationMatrix = mesh.transform.worldTransform.toArray(true); - } - if (mesh.shader.uniforms.uTextureMatrix) - { - if (mesh.uploadUvMatrix) - { - mesh.shader.uniforms.uTextureMatrix = mesh.uvMatrix.mapCoord.toArray(true); - } - else - { - mesh.shader.uniforms.uTextureMatrix = Matrix.IDENTITY.toArray(true); - } - } - - // bind and sync uniforms.. - this.renderer.shader.bind(mesh.shader); - - // set state.. - this.renderer.state.setState(mesh.state); - - // bind the geometry... - this.renderer.geometry.bind(mesh.geometry, mesh.shader); - // then render it - this.renderer.geometry.draw(mesh.drawMode, mesh.size, mesh.start, mesh.geometry.instanceCount); - } -} diff --git a/packages/mesh/src/index.js b/packages/mesh/src/index.js index e14ab9f..9e6dcd8 100644 --- a/packages/mesh/src/index.js +++ b/packages/mesh/src/index.js @@ -1,2 +1,4 @@ export { default as Mesh } from './Mesh'; -export { default as MeshRenderer } from './MeshRenderer'; +export { default as MeshMaterial } from './MeshMaterial'; +export { default as MeshGeometry } from './MeshGeometry'; + diff --git a/packages/mesh/src/shader/mesh.frag b/packages/mesh/src/shader/mesh.frag new file mode 100644 index 0000000..6096983 --- /dev/null +++ b/packages/mesh/src/shader/mesh.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +uniform vec4 uColor; + +uniform sampler2D uSampler; + +void main(void) +{ + gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; +} diff --git a/packages/mesh/src/shader/mesh.vert b/packages/mesh/src/shader/mesh.vert new file mode 100644 index 0000000..0526df9 --- /dev/null +++ b/packages/mesh/src/shader/mesh.vert @@ -0,0 +1,15 @@ +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTextureMatrix; + +varying vec2 vTextureCoord; + +void main(void) +{ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; +} diff --git a/packages/sprite/README.md b/packages/sprite/README.md index 3783526..1972ceb 100644 --- a/packages/sprite/README.md +++ b/packages/sprite/README.md @@ -9,8 +9,7 @@ ## Usage ```js -import { SpriteRenderer } from '@pixi/sprite'; -import { Renderer } from '@pixi/core'; +import { Sprite } from '@pixi/sprite'; -Renderer.registerPlugin('sprite', SpriteRenderer); +const sprite = new Sprite(); ``` \ No newline at end of file diff --git a/packages/sprite/src/BatchBuffer.js b/packages/sprite/src/BatchBuffer.js deleted file mode 100644 index cbc4c72..0000000 --- a/packages/sprite/src/BatchBuffer.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.positions = null; - this.uvs = null; - this.colors = null; - } -} diff --git a/packages/sprite/src/Sprite.js b/packages/sprite/src/Sprite.js index 91546cf..7b66e6e 100644 --- a/packages/sprite/src/Sprite.js +++ b/packages/sprite/src/Sprite.js @@ -5,10 +5,11 @@ import { Container } from '@pixi/display'; const tempPoint = new Point(); +const indices = new Uint16Array([0, 1, 2, 0, 2, 3]); /** * The Sprite object is the base for all textured objects that are rendered to the screen - * +* * A sprite can be created directly from an image like this: * * ```js @@ -119,6 +120,8 @@ */ this.cachedTint = 0xFFFFFF; + this.uvs = null; + // call texture setter this.texture = texture || Texture.EMPTY; @@ -144,6 +147,12 @@ this._transformTrimmedID = -1; this._textureTrimmedID = -1; + // Batchable stuff.. + // TODO could make this a mixin? + this.indices = indices; + this.size = 4; + this.start = 0; + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. @@ -151,7 +160,12 @@ * @member {string} * @default 'sprite' */ - this.pluginName = 'sprite'; + this.pluginName = 'batch'; + + /** + * used to fast check if a sprite is.. a sprite! + */ + this.isSprite = true; } /** @@ -165,6 +179,7 @@ this._textureTrimmedID = -1; this.cachedTint = 0xFFFFFF; + this.uvs = this._texture._uvs.uvsFloat32; // so if _width is 0 then width was not set.. if (this._width) { diff --git a/packages/sprite/src/SpriteRenderer.js b/packages/sprite/src/SpriteRenderer.js deleted file mode 100644 index 136cdcc..0000000 --- a/packages/sprite/src/SpriteRenderer.js +++ /dev/null @@ -1,463 +0,0 @@ -import { Geometry, - Buffer, - ObjectRenderer, - checkMaxIfStatementsInShader } from '@pixi/core'; -import { settings } from '@pixi/settings'; -import { createIndicesForQuads, premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; -import bitTwiddle from 'bit-twiddle'; -import BatchBuffer from './BatchBuffer'; -import generateMultiTextureShader from './generateMultiTextureShader'; -import { ENV } from '@pixi/constants'; - -let TICK = 0; -// const TEXTURE_TICK = 0; - -/** - * Renderer dedicated to drawing and batching sprites. - * - * @class - * @private - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class SpriteRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. - */ - constructor(renderer) - { - super(renderer); - - /** - * Number of values sent in the vertex buffer. - * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 - * - * @member {number} - */ - this.vertSize = 5; - - /** - * The size of the vertex information in bytes. - * - * @member {number} - */ - this.vertByteSize = this.vertSize * 4; - - /** - * The number of images in the SpriteRenderer before it flushes. - * - * @member {number} - */ - this.size = settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop - - // the total number of bytes in our batch - // let numVerts = this.size * 4 * this.vertByteSize; - - this.buffers = []; - for (let i = 1; i <= bitTwiddle.nextPow2(this.size); i *= 2) - { - this.buffers.push(new BatchBuffer(i * 4 * this.vertByteSize)); - } - - /** - * Holds the indices of the geometry (quads) to draw - * - * @member {Uint16Array} - */ - this.indices = createIndicesForQuads(this.size); - this.indexBuffer = new Buffer(this.indices, true, true); - - /** - * The default shaders that is used if a sprite doesn't have a more specific one. - * there is a shader for each number of textures that can be rendered. - * These shaders will also be generated on the fly as required. - * @member {PIXI.Shader[]} - */ - this.shader = null; - - this.currentIndex = 0; - this.groups = []; - - for (let k = 0; k < this.size; k++) - { - this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; - } - - this.sprites = []; - - this.vertexBuffers = []; - this.vaos = []; - - this.vaoMax = 2; - this.vertexCount = 0; - - this.renderer.on('prerender', this.onPrerender, this); - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - const gl = this.renderer.gl; - - if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) - { - this.MAX_TEXTURES = 1; - } - else - { - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); - } - - // generate generateMultiTextureProgram, may be a better move? - this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); - - // we use the second shader as the first one depending on your browser may omit aTextureId - // as it is not used by the shader so is optimized out. - for (let i = 0; i < this.vaoMax; i++) - { - const buffer = new Buffer(null, false); - - /* eslint-disable max-len */ - this.vaos[i] = new Geometry() - .addAttribute('aVertexPosition', buffer, 2, false, gl.FLOAT) - .addAttribute('aTextureCoord', buffer, 2, true, gl.UNSIGNED_SHORT) - .addAttribute('aColor', buffer, 4, true, gl.UNSIGNED_BYTE) - .addAttribute('aTextureId', buffer, 1, true, gl.FLOAT) - .addIndex(this.indexBuffer); - /* eslint-enable max-len */ - - this.vertexBuffers[i] = buffer; - } - } - - /** - * Called before the renderer starts rendering. - * - */ - onPrerender() - { - this.vertexCount = 0; - } - - /** - * Renders the sprite object. - * - * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch - */ - render(sprite) - { - // TODO set blend modes.. - // check texture.. - if (this.currentIndex >= this.size) - { - this.flush(); - } - - // get the uvs for the texture - - // if the uvs have not updated then no point rendering just yet! - if (!sprite._texture._uvs) - { - return; - } - - // push a texture. - // increment the batchsize - this.sprites[this.currentIndex++] = sprite; - } - - /** - * Renders the content and empties the current batch. - * - */ - flush() - { - if (this.currentIndex === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - - const np2 = bitTwiddle.nextPow2(this.currentIndex); - const log2 = bitTwiddle.log2(np2); - const buffer = this.buffers[log2]; - - const sprites = this.sprites; - const groups = this.groups; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let nextTexture; - let currentTexture; - let groupCount = 1; - let textureId = 0; - let textureCount = 0; - let currentGroup = groups[0]; - let vertexData; - let uvs; - let blendMode = premultiplyBlendMode[ - sprites[0]._texture.baseTexture.premultiplyAlpha ? 1 : 0][sprites[0].blendMode]; - - currentGroup.textureCount = 0; - currentGroup.start = 0; - currentGroup.blend = blendMode; - - TICK++; - - let i; - - for (i = 0; i < this.currentIndex; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - const sprite = sprites[i]; - - nextTexture = sprite._texture.baseTexture; - textureId = nextTexture._id; - - const spriteBlendMode = premultiplyBlendMode[Number(nextTexture.premultiplyAlpha)][sprite.blendMode]; - - if (blendMode !== spriteBlendMode) - { - blendMode = spriteBlendMode; - - // force the batch to break! - currentTexture = null; - textureCount = MAX_TEXTURES; - TICK++; - } - - if (currentTexture !== nextTexture) - { - currentTexture = nextTexture; - - if (nextTexture._enabled !== TICK) - { - if (textureCount === MAX_TEXTURES) - { - TICK++; - - textureCount = 0; - - currentGroup.size = i - currentGroup.start; - - currentGroup = groups[groupCount++]; - currentGroup.textureCount = 0; - currentGroup.blend = blendMode; - currentGroup.start = i; - } - - nextTexture.touched = touch; - nextTexture._enabled = TICK; - nextTexture._id = textureCount; - - currentGroup.textures[currentGroup.textureCount++] = nextTexture; - textureCount++; - } - } - - vertexData = sprite.vertexData; - - // TODO this sum does not need to be set each frame.. - uvs = sprite._texture._uvs.uvsUint32; - textureId = nextTexture._id; - - if (this.renderer.roundPixels) - { - const resolution = this.renderer.resolution; - - // xy - float32View[index] = ((vertexData[0] * resolution) | 0) / resolution; - float32View[index + 1] = ((vertexData[1] * resolution) | 0) / resolution; - - // xy - float32View[index + 5] = ((vertexData[2] * resolution) | 0) / resolution; - float32View[index + 6] = ((vertexData[3] * resolution) | 0) / resolution; - - // xy - float32View[index + 10] = ((vertexData[4] * resolution) | 0) / resolution; - float32View[index + 11] = ((vertexData[5] * resolution) | 0) / resolution; - - // xy - float32View[index + 15] = ((vertexData[6] * resolution) | 0) / resolution; - float32View[index + 16] = ((vertexData[7] * resolution) | 0) / resolution; - } - else - { - // xy - float32View[index] = vertexData[0]; - float32View[index + 1] = vertexData[1]; - - // xy - float32View[index + 5] = vertexData[2]; - float32View[index + 6] = vertexData[3]; - - // xy - float32View[index + 10] = vertexData[4]; - float32View[index + 11] = vertexData[5]; - - // xy - float32View[index + 15] = vertexData[6]; - float32View[index + 16] = vertexData[7]; - } - - uint32View[index + 2] = uvs[0]; - uint32View[index + 7] = uvs[1]; - uint32View[index + 12] = uvs[2]; - uint32View[index + 17] = uvs[3]; - /* eslint-disable max-len */ - const alpha = Math.min(sprite.worldAlpha, 1.0); - const argb = alpha < 1.0 && nextTexture.premultiplyAlpha ? premultiplyTint(sprite._tintRGB, alpha) - : sprite._tintRGB + (alpha * 255 << 24); - - uint32View[index + 3] = uint32View[index + 8] = uint32View[index + 13] = uint32View[index + 18] = argb; - - float32View[index + 4] = float32View[index + 9] = float32View[index + 14] = float32View[index + 19] = textureId; - /* eslint-enable max-len */ - - index += 20; - } - - currentGroup.size = i - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this.vaoMax <= this.vertexCount) - { - this.vaoMax++; - - const buffer = new Buffer(null, false); - - /* eslint-disable max-len */ - this.vaos[this.vertexCount] = new Geometry() - .addAttribute('aVertexPosition', buffer, 2, false, gl.FLOAT) - .addAttribute('aTextureCoord', buffer, 2, true, gl.UNSIGNED_SHORT) - .addAttribute('aColor', buffer, 4, true, gl.UNSIGNED_BYTE) - .addAttribute('aTextureId', buffer, 1, true, gl.FLOAT) - .addIndex(this.indexBuffer); - /* eslint-enable max-len */ - - this.vertexBuffers[this.vertexCount] = buffer; - } - - this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); - this.renderer.geometry.bind(this.vaos[this.vertexCount]); - - this.vertexCount++; - } - else - { - // lets use the faster option, always use buffer number 0 - this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); - - this.renderer.geometry.updateBuffers(); - } - - // / render the groups.. - for (i = 0; i < groupCount; i++) - { - const group = groups[i]; - const groupTextureCount = group.textureCount; - - for (let j = 0; j < groupTextureCount; j++) - { - this.renderer.texture.bind(group.textures[j], j); - } - - // set the blend mode.. - this.renderer.state.setBlendMode(group.blend); - - gl.drawElements(gl.TRIANGLES, group.size * 6, gl.UNSIGNED_SHORT, group.start * 6 * 2); - } - - // reset elements for the next flush - this.currentIndex = 0; - } - - /** - * Starts a new sprite batch. - */ - start() - { - this.renderer.shader.bind(this.shader); - - if (settings.CAN_UPLOAD_SAME_BUFFER) - { - // bind buffer #0, we don't need others - this.renderer.geometry.bind(this.vaos[this.vertexCount]); - } - } - - /** - * Stops and flushes the current batch. - * - */ - stop() - { - this.flush(); - } - - /** - * Destroys the SpriteRenderer. - * - */ - destroy() - { - for (let i = 0; i < this.vaoMax; i++) - { - if (this.vertexBuffers[i]) - { - this.vertexBuffers[i].destroy(); - } - if (this.vaos[i]) - { - this.vaos[i].destroy(); - } - } - - if (this.indexBuffer) - { - this.indexBuffer.destroy(); - } - - this.renderer.off('prerender', this.onPrerender, this); - - if (this.shader) - { - this.shader.destroy(); - this.shader = null; - } - - this.vertexBuffers = null; - this.vaos = null; - this.indexBuffer = null; - this.indices = null; - - this.sprites = null; - - for (let i = 0; i < this.buffers.length; ++i) - { - this.buffers[i].destroy(); - } - - super.destroy(); - } -} diff --git a/packages/sprite/src/generateMultiTextureShader.js b/packages/sprite/src/generateMultiTextureShader.js deleted file mode 100644 index f2e27be..0000000 --- a/packages/sprite/src/generateMultiTextureShader.js +++ /dev/null @@ -1,69 +0,0 @@ -import { Shader, UniformGroup } from '@pixi/core'; -import vertex from './texture.vert'; - -const fragTemplate = [ - 'varying vec2 vTextureCoord;', - 'varying vec4 vColor;', - 'varying float vTextureId;', - 'uniform sampler2D uSamplers[%count%];', - - 'void main(void){', - 'vec4 color;', - 'float textureId = floor(vTextureId+0.5);', - '%forloop%', - 'gl_FragColor = color * vColor;', - '}', -].join('\n'); - -export default function generateMultiTextureShader(gl, maxTextures) -{ - const sampleValues = new Int32Array(maxTextures); - - for (let i = 0; i < maxTextures; i++) - { - sampleValues[i] = i; - } - - const uniforms = { - default: UniformGroup.from({ uSamplers: sampleValues }, true), - }; - - let fragmentSrc = fragTemplate; - - fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); - fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); - - const shader = Shader.from(vertex, fragmentSrc, uniforms); - - return shader; -} - -function generateSampleSrc(maxTextures) -{ - let src = ''; - - src += '\n'; - src += '\n'; - - for (let i = 0; i < maxTextures; i++) - { - if (i > 0) - { - src += '\nelse '; - } - - if (i < maxTextures - 1) - { - src += `if(textureId == ${i}.0)`; - } - - src += '\n{'; - src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; - src += '\n}'; - } - - src += '\n'; - src += '\n'; - - return src; -} diff --git a/packages/sprite/src/index.js b/packages/sprite/src/index.js index edf477c..c5179d7 100644 --- a/packages/sprite/src/index.js +++ b/packages/sprite/src/index.js @@ -1,2 +1 @@ export { default as Sprite } from './Sprite'; -export { default as SpriteRenderer } from './SpriteRenderer'; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/packages/mesh/src/MeshGeometry.js b/packages/mesh/src/MeshGeometry.js new file mode 100644 index 0000000..ae6c702 --- /dev/null +++ b/packages/mesh/src/MeshGeometry.js @@ -0,0 +1,61 @@ +import { TYPES } from '@pixi/constants'; +import { Buffer, Geometry } from '@pixi/core'; + +/** + * Standard 2D geometry used in PixiJS. + * + * Geometry can be defined without passing in a style or data if required. + * + * ```js + * const geometry = new PIXI.Geometry(); + * + * geometry.addAttribute('positions', [0, 0, 100, 0, 100, 100, 0, 100], 2); + * geometry.addAttribute('uvs', [0,0,1,0,1,1,0,1], 2); + * geometry.addIndex([0,1,2,1,3,2]); + * + * ``` + * @class + * @memberof PIXI + * @extends PIXI.Geometry + */ +export default class MeshGeometry extends Geometry +{ + /** + * @param {Float32Array|number[]} vertices - Positional data on geometry. + * @param {Float32Array|number[]} uvs - Texture UVs. + * @param {Uint16Array|number[]} index - IndexBuffer + */ + constructor(vertices, uvs, index) + { + super(); + + const verticesBuffer = new Buffer(vertices); + const uvsBuffer = new Buffer(uvs, true); + const indexBuffer = new Buffer(index, true, true); + + this.addAttribute('aVertexPosition', verticesBuffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', uvsBuffer, 2, false, TYPES.FLOAT) + .addIndex(indexBuffer); + + /** + * Dirty flag to limit update calls on Mesh. For example, + * limiting updates on a single Mesh instance with a shared Geometry + * within the render loop. + * @private + * @member {number} + * @default -1 + */ + this._updateId = -1; + } + + /** + * If the vertex position is updated. + * @member {number} + * @readonly + * @private + */ + get vertexDirtyId() + { + return this.buffers[0]._updateID; + } +} diff --git a/packages/mesh/src/MeshMaterial.js b/packages/mesh/src/MeshMaterial.js new file mode 100644 index 0000000..028cf49 --- /dev/null +++ b/packages/mesh/src/MeshMaterial.js @@ -0,0 +1,130 @@ +import { Shader, Program, TextureMatrix } from '@pixi/core'; +import vertex from './shader/mesh.vert'; +import fragment from './shader/mesh.frag'; +import { Matrix } from '@pixi/math'; +import { premultiplyTintToRgba } from '@pixi/utils'; + +/** + * Slightly opinionated default shader for PixiJS 2D objects. + * @class + * @memberof PIXI + * @extends PIXI.Shader + */ +export default class MeshMaterial extends Shader +{ + /** + * @param {PIXI.Texture} uSampler - Texture that material uses to render. + * @param {object} [options] - Additional options + * @param {number} [options.alpha=1] - Default alpha. + * @param {number} [options.tint=0xFFFFFF] - Default tint. + */ + constructor(uSampler, options) + { + const program = Program.from(vertex, fragment); + + const uniforms = { + uSampler, + alpha: 1, + uTextureMatrix: Matrix.IDENTITY, + uColor: new Float32Array([1, 1, 1, 1]), + }; + + super(program, uniforms); + + /** + * Only do update if tint or alpha changes. + * @member {boolean} + * @private + * @default false + */ + this._colorDirty = false; + + /** + * TextureMatrix instance for this Mesh, used to track Texture changes + * + * @member {PIXI.TextureMatrix} + * @readonly + */ + // TODO get this back in! + this.uvMatrix = new TextureMatrix(this.uSampler); + + /** + * `true` if shader can be batch with the renderer's batch system. + * @member {boolean} + * @default true + */ + this.batchable = true; + + // Set defaults + const { tint, alpha } = Object.assign({ + tint: 0xFFFFFF, + alpha: 1, + }, options); + + this.tint = tint; + this.alpha = alpha; + } + + /** + * Reference to the texture being rendered. + * @member {PIXI.Texture} + */ + get texture() + { + return this.uniforms.uSampler; + } + set texture(value) + { + this.uniforms.uSampler = value; + } + + /** + * This gets automatically set by the object using this. + * @default 1 + * @member {number} + */ + set alpha(value) + { + if (value === this._alpha) return; + + this._alpha = value; + this._colorDirty = true; + } + get alpha() + { + return this._alpha; + } + + /** + * Multiply tint for the material. + * @member {number} + * @default 0xFFFFFF + */ + set tint(value) + { + if (value === this._tint) return; + + this._tint = value; + this._tintRGB = (value >> 16) + (value & 0xff00) + ((value & 0xff) << 16); + this._colorDirty = true; + } + get tint() + { + return this._tint; + } + + /** + * Gets called automatically by the Mesh. Intended to be overridden for custom + * MeshMaterial objects. + */ + update() + { + if (this._colorDirty) + { + this._colorDirty = false; + const baseTexture = this.texture.baseTexture; + + premultiplyTintToRgba(this._tintRGB, this._alpha, this.uniforms.uColor, baseTexture.premultiplyAlpha); + } + } +} diff --git a/packages/mesh/src/MeshRenderer.js b/packages/mesh/src/MeshRenderer.js deleted file mode 100644 index d7baf9b..0000000 --- a/packages/mesh/src/MeshRenderer.js +++ /dev/null @@ -1,77 +0,0 @@ -import { ObjectRenderer } from '@pixi/core'; -import { Matrix } from '@pixi/math'; - -/** - * WebGL renderer plugin for tiling sprites - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class MeshRenderer extends ObjectRenderer -{ - /** - * constructor for renderer - * - * @param {Renderer} renderer The renderer this tiling awesomeness works for. - */ - constructor(renderer) - { - super(renderer); - - this.shader = null; - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * renders mesh - * @private - * @param {PIXI.Mesh} mesh mesh instance - */ - render(mesh) - { - // bind the shader.. - - // TODO - // set the shader props.. - // probably only need to set once! - // as its then a reference.. - if (mesh.shader.program.uniformData.translationMatrix) - { - // the transform! - mesh.shader.uniforms.translationMatrix = mesh.transform.worldTransform.toArray(true); - } - if (mesh.shader.uniforms.uTextureMatrix) - { - if (mesh.uploadUvMatrix) - { - mesh.shader.uniforms.uTextureMatrix = mesh.uvMatrix.mapCoord.toArray(true); - } - else - { - mesh.shader.uniforms.uTextureMatrix = Matrix.IDENTITY.toArray(true); - } - } - - // bind and sync uniforms.. - this.renderer.shader.bind(mesh.shader); - - // set state.. - this.renderer.state.setState(mesh.state); - - // bind the geometry... - this.renderer.geometry.bind(mesh.geometry, mesh.shader); - // then render it - this.renderer.geometry.draw(mesh.drawMode, mesh.size, mesh.start, mesh.geometry.instanceCount); - } -} diff --git a/packages/mesh/src/index.js b/packages/mesh/src/index.js index e14ab9f..9e6dcd8 100644 --- a/packages/mesh/src/index.js +++ b/packages/mesh/src/index.js @@ -1,2 +1,4 @@ export { default as Mesh } from './Mesh'; -export { default as MeshRenderer } from './MeshRenderer'; +export { default as MeshMaterial } from './MeshMaterial'; +export { default as MeshGeometry } from './MeshGeometry'; + diff --git a/packages/mesh/src/shader/mesh.frag b/packages/mesh/src/shader/mesh.frag new file mode 100644 index 0000000..6096983 --- /dev/null +++ b/packages/mesh/src/shader/mesh.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +uniform vec4 uColor; + +uniform sampler2D uSampler; + +void main(void) +{ + gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; +} diff --git a/packages/mesh/src/shader/mesh.vert b/packages/mesh/src/shader/mesh.vert new file mode 100644 index 0000000..0526df9 --- /dev/null +++ b/packages/mesh/src/shader/mesh.vert @@ -0,0 +1,15 @@ +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTextureMatrix; + +varying vec2 vTextureCoord; + +void main(void) +{ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; +} diff --git a/packages/sprite/README.md b/packages/sprite/README.md index 3783526..1972ceb 100644 --- a/packages/sprite/README.md +++ b/packages/sprite/README.md @@ -9,8 +9,7 @@ ## Usage ```js -import { SpriteRenderer } from '@pixi/sprite'; -import { Renderer } from '@pixi/core'; +import { Sprite } from '@pixi/sprite'; -Renderer.registerPlugin('sprite', SpriteRenderer); +const sprite = new Sprite(); ``` \ No newline at end of file diff --git a/packages/sprite/src/BatchBuffer.js b/packages/sprite/src/BatchBuffer.js deleted file mode 100644 index cbc4c72..0000000 --- a/packages/sprite/src/BatchBuffer.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.positions = null; - this.uvs = null; - this.colors = null; - } -} diff --git a/packages/sprite/src/Sprite.js b/packages/sprite/src/Sprite.js index 91546cf..7b66e6e 100644 --- a/packages/sprite/src/Sprite.js +++ b/packages/sprite/src/Sprite.js @@ -5,10 +5,11 @@ import { Container } from '@pixi/display'; const tempPoint = new Point(); +const indices = new Uint16Array([0, 1, 2, 0, 2, 3]); /** * The Sprite object is the base for all textured objects that are rendered to the screen - * +* * A sprite can be created directly from an image like this: * * ```js @@ -119,6 +120,8 @@ */ this.cachedTint = 0xFFFFFF; + this.uvs = null; + // call texture setter this.texture = texture || Texture.EMPTY; @@ -144,6 +147,12 @@ this._transformTrimmedID = -1; this._textureTrimmedID = -1; + // Batchable stuff.. + // TODO could make this a mixin? + this.indices = indices; + this.size = 4; + this.start = 0; + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. @@ -151,7 +160,12 @@ * @member {string} * @default 'sprite' */ - this.pluginName = 'sprite'; + this.pluginName = 'batch'; + + /** + * used to fast check if a sprite is.. a sprite! + */ + this.isSprite = true; } /** @@ -165,6 +179,7 @@ this._textureTrimmedID = -1; this.cachedTint = 0xFFFFFF; + this.uvs = this._texture._uvs.uvsFloat32; // so if _width is 0 then width was not set.. if (this._width) { diff --git a/packages/sprite/src/SpriteRenderer.js b/packages/sprite/src/SpriteRenderer.js deleted file mode 100644 index 136cdcc..0000000 --- a/packages/sprite/src/SpriteRenderer.js +++ /dev/null @@ -1,463 +0,0 @@ -import { Geometry, - Buffer, - ObjectRenderer, - checkMaxIfStatementsInShader } from '@pixi/core'; -import { settings } from '@pixi/settings'; -import { createIndicesForQuads, premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; -import bitTwiddle from 'bit-twiddle'; -import BatchBuffer from './BatchBuffer'; -import generateMultiTextureShader from './generateMultiTextureShader'; -import { ENV } from '@pixi/constants'; - -let TICK = 0; -// const TEXTURE_TICK = 0; - -/** - * Renderer dedicated to drawing and batching sprites. - * - * @class - * @private - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class SpriteRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. - */ - constructor(renderer) - { - super(renderer); - - /** - * Number of values sent in the vertex buffer. - * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 - * - * @member {number} - */ - this.vertSize = 5; - - /** - * The size of the vertex information in bytes. - * - * @member {number} - */ - this.vertByteSize = this.vertSize * 4; - - /** - * The number of images in the SpriteRenderer before it flushes. - * - * @member {number} - */ - this.size = settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop - - // the total number of bytes in our batch - // let numVerts = this.size * 4 * this.vertByteSize; - - this.buffers = []; - for (let i = 1; i <= bitTwiddle.nextPow2(this.size); i *= 2) - { - this.buffers.push(new BatchBuffer(i * 4 * this.vertByteSize)); - } - - /** - * Holds the indices of the geometry (quads) to draw - * - * @member {Uint16Array} - */ - this.indices = createIndicesForQuads(this.size); - this.indexBuffer = new Buffer(this.indices, true, true); - - /** - * The default shaders that is used if a sprite doesn't have a more specific one. - * there is a shader for each number of textures that can be rendered. - * These shaders will also be generated on the fly as required. - * @member {PIXI.Shader[]} - */ - this.shader = null; - - this.currentIndex = 0; - this.groups = []; - - for (let k = 0; k < this.size; k++) - { - this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; - } - - this.sprites = []; - - this.vertexBuffers = []; - this.vaos = []; - - this.vaoMax = 2; - this.vertexCount = 0; - - this.renderer.on('prerender', this.onPrerender, this); - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - const gl = this.renderer.gl; - - if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) - { - this.MAX_TEXTURES = 1; - } - else - { - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); - } - - // generate generateMultiTextureProgram, may be a better move? - this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); - - // we use the second shader as the first one depending on your browser may omit aTextureId - // as it is not used by the shader so is optimized out. - for (let i = 0; i < this.vaoMax; i++) - { - const buffer = new Buffer(null, false); - - /* eslint-disable max-len */ - this.vaos[i] = new Geometry() - .addAttribute('aVertexPosition', buffer, 2, false, gl.FLOAT) - .addAttribute('aTextureCoord', buffer, 2, true, gl.UNSIGNED_SHORT) - .addAttribute('aColor', buffer, 4, true, gl.UNSIGNED_BYTE) - .addAttribute('aTextureId', buffer, 1, true, gl.FLOAT) - .addIndex(this.indexBuffer); - /* eslint-enable max-len */ - - this.vertexBuffers[i] = buffer; - } - } - - /** - * Called before the renderer starts rendering. - * - */ - onPrerender() - { - this.vertexCount = 0; - } - - /** - * Renders the sprite object. - * - * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch - */ - render(sprite) - { - // TODO set blend modes.. - // check texture.. - if (this.currentIndex >= this.size) - { - this.flush(); - } - - // get the uvs for the texture - - // if the uvs have not updated then no point rendering just yet! - if (!sprite._texture._uvs) - { - return; - } - - // push a texture. - // increment the batchsize - this.sprites[this.currentIndex++] = sprite; - } - - /** - * Renders the content and empties the current batch. - * - */ - flush() - { - if (this.currentIndex === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - - const np2 = bitTwiddle.nextPow2(this.currentIndex); - const log2 = bitTwiddle.log2(np2); - const buffer = this.buffers[log2]; - - const sprites = this.sprites; - const groups = this.groups; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let nextTexture; - let currentTexture; - let groupCount = 1; - let textureId = 0; - let textureCount = 0; - let currentGroup = groups[0]; - let vertexData; - let uvs; - let blendMode = premultiplyBlendMode[ - sprites[0]._texture.baseTexture.premultiplyAlpha ? 1 : 0][sprites[0].blendMode]; - - currentGroup.textureCount = 0; - currentGroup.start = 0; - currentGroup.blend = blendMode; - - TICK++; - - let i; - - for (i = 0; i < this.currentIndex; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - const sprite = sprites[i]; - - nextTexture = sprite._texture.baseTexture; - textureId = nextTexture._id; - - const spriteBlendMode = premultiplyBlendMode[Number(nextTexture.premultiplyAlpha)][sprite.blendMode]; - - if (blendMode !== spriteBlendMode) - { - blendMode = spriteBlendMode; - - // force the batch to break! - currentTexture = null; - textureCount = MAX_TEXTURES; - TICK++; - } - - if (currentTexture !== nextTexture) - { - currentTexture = nextTexture; - - if (nextTexture._enabled !== TICK) - { - if (textureCount === MAX_TEXTURES) - { - TICK++; - - textureCount = 0; - - currentGroup.size = i - currentGroup.start; - - currentGroup = groups[groupCount++]; - currentGroup.textureCount = 0; - currentGroup.blend = blendMode; - currentGroup.start = i; - } - - nextTexture.touched = touch; - nextTexture._enabled = TICK; - nextTexture._id = textureCount; - - currentGroup.textures[currentGroup.textureCount++] = nextTexture; - textureCount++; - } - } - - vertexData = sprite.vertexData; - - // TODO this sum does not need to be set each frame.. - uvs = sprite._texture._uvs.uvsUint32; - textureId = nextTexture._id; - - if (this.renderer.roundPixels) - { - const resolution = this.renderer.resolution; - - // xy - float32View[index] = ((vertexData[0] * resolution) | 0) / resolution; - float32View[index + 1] = ((vertexData[1] * resolution) | 0) / resolution; - - // xy - float32View[index + 5] = ((vertexData[2] * resolution) | 0) / resolution; - float32View[index + 6] = ((vertexData[3] * resolution) | 0) / resolution; - - // xy - float32View[index + 10] = ((vertexData[4] * resolution) | 0) / resolution; - float32View[index + 11] = ((vertexData[5] * resolution) | 0) / resolution; - - // xy - float32View[index + 15] = ((vertexData[6] * resolution) | 0) / resolution; - float32View[index + 16] = ((vertexData[7] * resolution) | 0) / resolution; - } - else - { - // xy - float32View[index] = vertexData[0]; - float32View[index + 1] = vertexData[1]; - - // xy - float32View[index + 5] = vertexData[2]; - float32View[index + 6] = vertexData[3]; - - // xy - float32View[index + 10] = vertexData[4]; - float32View[index + 11] = vertexData[5]; - - // xy - float32View[index + 15] = vertexData[6]; - float32View[index + 16] = vertexData[7]; - } - - uint32View[index + 2] = uvs[0]; - uint32View[index + 7] = uvs[1]; - uint32View[index + 12] = uvs[2]; - uint32View[index + 17] = uvs[3]; - /* eslint-disable max-len */ - const alpha = Math.min(sprite.worldAlpha, 1.0); - const argb = alpha < 1.0 && nextTexture.premultiplyAlpha ? premultiplyTint(sprite._tintRGB, alpha) - : sprite._tintRGB + (alpha * 255 << 24); - - uint32View[index + 3] = uint32View[index + 8] = uint32View[index + 13] = uint32View[index + 18] = argb; - - float32View[index + 4] = float32View[index + 9] = float32View[index + 14] = float32View[index + 19] = textureId; - /* eslint-enable max-len */ - - index += 20; - } - - currentGroup.size = i - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this.vaoMax <= this.vertexCount) - { - this.vaoMax++; - - const buffer = new Buffer(null, false); - - /* eslint-disable max-len */ - this.vaos[this.vertexCount] = new Geometry() - .addAttribute('aVertexPosition', buffer, 2, false, gl.FLOAT) - .addAttribute('aTextureCoord', buffer, 2, true, gl.UNSIGNED_SHORT) - .addAttribute('aColor', buffer, 4, true, gl.UNSIGNED_BYTE) - .addAttribute('aTextureId', buffer, 1, true, gl.FLOAT) - .addIndex(this.indexBuffer); - /* eslint-enable max-len */ - - this.vertexBuffers[this.vertexCount] = buffer; - } - - this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); - this.renderer.geometry.bind(this.vaos[this.vertexCount]); - - this.vertexCount++; - } - else - { - // lets use the faster option, always use buffer number 0 - this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); - - this.renderer.geometry.updateBuffers(); - } - - // / render the groups.. - for (i = 0; i < groupCount; i++) - { - const group = groups[i]; - const groupTextureCount = group.textureCount; - - for (let j = 0; j < groupTextureCount; j++) - { - this.renderer.texture.bind(group.textures[j], j); - } - - // set the blend mode.. - this.renderer.state.setBlendMode(group.blend); - - gl.drawElements(gl.TRIANGLES, group.size * 6, gl.UNSIGNED_SHORT, group.start * 6 * 2); - } - - // reset elements for the next flush - this.currentIndex = 0; - } - - /** - * Starts a new sprite batch. - */ - start() - { - this.renderer.shader.bind(this.shader); - - if (settings.CAN_UPLOAD_SAME_BUFFER) - { - // bind buffer #0, we don't need others - this.renderer.geometry.bind(this.vaos[this.vertexCount]); - } - } - - /** - * Stops and flushes the current batch. - * - */ - stop() - { - this.flush(); - } - - /** - * Destroys the SpriteRenderer. - * - */ - destroy() - { - for (let i = 0; i < this.vaoMax; i++) - { - if (this.vertexBuffers[i]) - { - this.vertexBuffers[i].destroy(); - } - if (this.vaos[i]) - { - this.vaos[i].destroy(); - } - } - - if (this.indexBuffer) - { - this.indexBuffer.destroy(); - } - - this.renderer.off('prerender', this.onPrerender, this); - - if (this.shader) - { - this.shader.destroy(); - this.shader = null; - } - - this.vertexBuffers = null; - this.vaos = null; - this.indexBuffer = null; - this.indices = null; - - this.sprites = null; - - for (let i = 0; i < this.buffers.length; ++i) - { - this.buffers[i].destroy(); - } - - super.destroy(); - } -} diff --git a/packages/sprite/src/generateMultiTextureShader.js b/packages/sprite/src/generateMultiTextureShader.js deleted file mode 100644 index f2e27be..0000000 --- a/packages/sprite/src/generateMultiTextureShader.js +++ /dev/null @@ -1,69 +0,0 @@ -import { Shader, UniformGroup } from '@pixi/core'; -import vertex from './texture.vert'; - -const fragTemplate = [ - 'varying vec2 vTextureCoord;', - 'varying vec4 vColor;', - 'varying float vTextureId;', - 'uniform sampler2D uSamplers[%count%];', - - 'void main(void){', - 'vec4 color;', - 'float textureId = floor(vTextureId+0.5);', - '%forloop%', - 'gl_FragColor = color * vColor;', - '}', -].join('\n'); - -export default function generateMultiTextureShader(gl, maxTextures) -{ - const sampleValues = new Int32Array(maxTextures); - - for (let i = 0; i < maxTextures; i++) - { - sampleValues[i] = i; - } - - const uniforms = { - default: UniformGroup.from({ uSamplers: sampleValues }, true), - }; - - let fragmentSrc = fragTemplate; - - fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); - fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); - - const shader = Shader.from(vertex, fragmentSrc, uniforms); - - return shader; -} - -function generateSampleSrc(maxTextures) -{ - let src = ''; - - src += '\n'; - src += '\n'; - - for (let i = 0; i < maxTextures; i++) - { - if (i > 0) - { - src += '\nelse '; - } - - if (i < maxTextures - 1) - { - src += `if(textureId == ${i}.0)`; - } - - src += '\n{'; - src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; - src += '\n}'; - } - - src += '\n'; - src += '\n'; - - return src; -} diff --git a/packages/sprite/src/index.js b/packages/sprite/src/index.js index edf477c..c5179d7 100644 --- a/packages/sprite/src/index.js +++ b/packages/sprite/src/index.js @@ -1,2 +1 @@ export { default as Sprite } from './Sprite'; -export { default as SpriteRenderer } from './SpriteRenderer'; diff --git a/packages/sprite/src/texture.vert b/packages/sprite/src/texture.vert deleted file mode 100644 index 18b89ff..0000000 --- a/packages/sprite/src/texture.vert +++ /dev/null @@ -1,19 +0,0 @@ -precision highp float; -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; -attribute vec4 aColor; -attribute float aTextureId; - -uniform mat3 projectionMatrix; - -varying vec2 vTextureCoord; -varying vec4 vColor; -varying float vTextureId; - -void main(void){ - gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = aTextureCoord; - vTextureId = aTextureId; - vColor = aColor; -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/packages/mesh/src/MeshGeometry.js b/packages/mesh/src/MeshGeometry.js new file mode 100644 index 0000000..ae6c702 --- /dev/null +++ b/packages/mesh/src/MeshGeometry.js @@ -0,0 +1,61 @@ +import { TYPES } from '@pixi/constants'; +import { Buffer, Geometry } from '@pixi/core'; + +/** + * Standard 2D geometry used in PixiJS. + * + * Geometry can be defined without passing in a style or data if required. + * + * ```js + * const geometry = new PIXI.Geometry(); + * + * geometry.addAttribute('positions', [0, 0, 100, 0, 100, 100, 0, 100], 2); + * geometry.addAttribute('uvs', [0,0,1,0,1,1,0,1], 2); + * geometry.addIndex([0,1,2,1,3,2]); + * + * ``` + * @class + * @memberof PIXI + * @extends PIXI.Geometry + */ +export default class MeshGeometry extends Geometry +{ + /** + * @param {Float32Array|number[]} vertices - Positional data on geometry. + * @param {Float32Array|number[]} uvs - Texture UVs. + * @param {Uint16Array|number[]} index - IndexBuffer + */ + constructor(vertices, uvs, index) + { + super(); + + const verticesBuffer = new Buffer(vertices); + const uvsBuffer = new Buffer(uvs, true); + const indexBuffer = new Buffer(index, true, true); + + this.addAttribute('aVertexPosition', verticesBuffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', uvsBuffer, 2, false, TYPES.FLOAT) + .addIndex(indexBuffer); + + /** + * Dirty flag to limit update calls on Mesh. For example, + * limiting updates on a single Mesh instance with a shared Geometry + * within the render loop. + * @private + * @member {number} + * @default -1 + */ + this._updateId = -1; + } + + /** + * If the vertex position is updated. + * @member {number} + * @readonly + * @private + */ + get vertexDirtyId() + { + return this.buffers[0]._updateID; + } +} diff --git a/packages/mesh/src/MeshMaterial.js b/packages/mesh/src/MeshMaterial.js new file mode 100644 index 0000000..028cf49 --- /dev/null +++ b/packages/mesh/src/MeshMaterial.js @@ -0,0 +1,130 @@ +import { Shader, Program, TextureMatrix } from '@pixi/core'; +import vertex from './shader/mesh.vert'; +import fragment from './shader/mesh.frag'; +import { Matrix } from '@pixi/math'; +import { premultiplyTintToRgba } from '@pixi/utils'; + +/** + * Slightly opinionated default shader for PixiJS 2D objects. + * @class + * @memberof PIXI + * @extends PIXI.Shader + */ +export default class MeshMaterial extends Shader +{ + /** + * @param {PIXI.Texture} uSampler - Texture that material uses to render. + * @param {object} [options] - Additional options + * @param {number} [options.alpha=1] - Default alpha. + * @param {number} [options.tint=0xFFFFFF] - Default tint. + */ + constructor(uSampler, options) + { + const program = Program.from(vertex, fragment); + + const uniforms = { + uSampler, + alpha: 1, + uTextureMatrix: Matrix.IDENTITY, + uColor: new Float32Array([1, 1, 1, 1]), + }; + + super(program, uniforms); + + /** + * Only do update if tint or alpha changes. + * @member {boolean} + * @private + * @default false + */ + this._colorDirty = false; + + /** + * TextureMatrix instance for this Mesh, used to track Texture changes + * + * @member {PIXI.TextureMatrix} + * @readonly + */ + // TODO get this back in! + this.uvMatrix = new TextureMatrix(this.uSampler); + + /** + * `true` if shader can be batch with the renderer's batch system. + * @member {boolean} + * @default true + */ + this.batchable = true; + + // Set defaults + const { tint, alpha } = Object.assign({ + tint: 0xFFFFFF, + alpha: 1, + }, options); + + this.tint = tint; + this.alpha = alpha; + } + + /** + * Reference to the texture being rendered. + * @member {PIXI.Texture} + */ + get texture() + { + return this.uniforms.uSampler; + } + set texture(value) + { + this.uniforms.uSampler = value; + } + + /** + * This gets automatically set by the object using this. + * @default 1 + * @member {number} + */ + set alpha(value) + { + if (value === this._alpha) return; + + this._alpha = value; + this._colorDirty = true; + } + get alpha() + { + return this._alpha; + } + + /** + * Multiply tint for the material. + * @member {number} + * @default 0xFFFFFF + */ + set tint(value) + { + if (value === this._tint) return; + + this._tint = value; + this._tintRGB = (value >> 16) + (value & 0xff00) + ((value & 0xff) << 16); + this._colorDirty = true; + } + get tint() + { + return this._tint; + } + + /** + * Gets called automatically by the Mesh. Intended to be overridden for custom + * MeshMaterial objects. + */ + update() + { + if (this._colorDirty) + { + this._colorDirty = false; + const baseTexture = this.texture.baseTexture; + + premultiplyTintToRgba(this._tintRGB, this._alpha, this.uniforms.uColor, baseTexture.premultiplyAlpha); + } + } +} diff --git a/packages/mesh/src/MeshRenderer.js b/packages/mesh/src/MeshRenderer.js deleted file mode 100644 index d7baf9b..0000000 --- a/packages/mesh/src/MeshRenderer.js +++ /dev/null @@ -1,77 +0,0 @@ -import { ObjectRenderer } from '@pixi/core'; -import { Matrix } from '@pixi/math'; - -/** - * WebGL renderer plugin for tiling sprites - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class MeshRenderer extends ObjectRenderer -{ - /** - * constructor for renderer - * - * @param {Renderer} renderer The renderer this tiling awesomeness works for. - */ - constructor(renderer) - { - super(renderer); - - this.shader = null; - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * renders mesh - * @private - * @param {PIXI.Mesh} mesh mesh instance - */ - render(mesh) - { - // bind the shader.. - - // TODO - // set the shader props.. - // probably only need to set once! - // as its then a reference.. - if (mesh.shader.program.uniformData.translationMatrix) - { - // the transform! - mesh.shader.uniforms.translationMatrix = mesh.transform.worldTransform.toArray(true); - } - if (mesh.shader.uniforms.uTextureMatrix) - { - if (mesh.uploadUvMatrix) - { - mesh.shader.uniforms.uTextureMatrix = mesh.uvMatrix.mapCoord.toArray(true); - } - else - { - mesh.shader.uniforms.uTextureMatrix = Matrix.IDENTITY.toArray(true); - } - } - - // bind and sync uniforms.. - this.renderer.shader.bind(mesh.shader); - - // set state.. - this.renderer.state.setState(mesh.state); - - // bind the geometry... - this.renderer.geometry.bind(mesh.geometry, mesh.shader); - // then render it - this.renderer.geometry.draw(mesh.drawMode, mesh.size, mesh.start, mesh.geometry.instanceCount); - } -} diff --git a/packages/mesh/src/index.js b/packages/mesh/src/index.js index e14ab9f..9e6dcd8 100644 --- a/packages/mesh/src/index.js +++ b/packages/mesh/src/index.js @@ -1,2 +1,4 @@ export { default as Mesh } from './Mesh'; -export { default as MeshRenderer } from './MeshRenderer'; +export { default as MeshMaterial } from './MeshMaterial'; +export { default as MeshGeometry } from './MeshGeometry'; + diff --git a/packages/mesh/src/shader/mesh.frag b/packages/mesh/src/shader/mesh.frag new file mode 100644 index 0000000..6096983 --- /dev/null +++ b/packages/mesh/src/shader/mesh.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +uniform vec4 uColor; + +uniform sampler2D uSampler; + +void main(void) +{ + gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; +} diff --git a/packages/mesh/src/shader/mesh.vert b/packages/mesh/src/shader/mesh.vert new file mode 100644 index 0000000..0526df9 --- /dev/null +++ b/packages/mesh/src/shader/mesh.vert @@ -0,0 +1,15 @@ +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTextureMatrix; + +varying vec2 vTextureCoord; + +void main(void) +{ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; +} diff --git a/packages/sprite/README.md b/packages/sprite/README.md index 3783526..1972ceb 100644 --- a/packages/sprite/README.md +++ b/packages/sprite/README.md @@ -9,8 +9,7 @@ ## Usage ```js -import { SpriteRenderer } from '@pixi/sprite'; -import { Renderer } from '@pixi/core'; +import { Sprite } from '@pixi/sprite'; -Renderer.registerPlugin('sprite', SpriteRenderer); +const sprite = new Sprite(); ``` \ No newline at end of file diff --git a/packages/sprite/src/BatchBuffer.js b/packages/sprite/src/BatchBuffer.js deleted file mode 100644 index cbc4c72..0000000 --- a/packages/sprite/src/BatchBuffer.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.positions = null; - this.uvs = null; - this.colors = null; - } -} diff --git a/packages/sprite/src/Sprite.js b/packages/sprite/src/Sprite.js index 91546cf..7b66e6e 100644 --- a/packages/sprite/src/Sprite.js +++ b/packages/sprite/src/Sprite.js @@ -5,10 +5,11 @@ import { Container } from '@pixi/display'; const tempPoint = new Point(); +const indices = new Uint16Array([0, 1, 2, 0, 2, 3]); /** * The Sprite object is the base for all textured objects that are rendered to the screen - * +* * A sprite can be created directly from an image like this: * * ```js @@ -119,6 +120,8 @@ */ this.cachedTint = 0xFFFFFF; + this.uvs = null; + // call texture setter this.texture = texture || Texture.EMPTY; @@ -144,6 +147,12 @@ this._transformTrimmedID = -1; this._textureTrimmedID = -1; + // Batchable stuff.. + // TODO could make this a mixin? + this.indices = indices; + this.size = 4; + this.start = 0; + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. @@ -151,7 +160,12 @@ * @member {string} * @default 'sprite' */ - this.pluginName = 'sprite'; + this.pluginName = 'batch'; + + /** + * used to fast check if a sprite is.. a sprite! + */ + this.isSprite = true; } /** @@ -165,6 +179,7 @@ this._textureTrimmedID = -1; this.cachedTint = 0xFFFFFF; + this.uvs = this._texture._uvs.uvsFloat32; // so if _width is 0 then width was not set.. if (this._width) { diff --git a/packages/sprite/src/SpriteRenderer.js b/packages/sprite/src/SpriteRenderer.js deleted file mode 100644 index 136cdcc..0000000 --- a/packages/sprite/src/SpriteRenderer.js +++ /dev/null @@ -1,463 +0,0 @@ -import { Geometry, - Buffer, - ObjectRenderer, - checkMaxIfStatementsInShader } from '@pixi/core'; -import { settings } from '@pixi/settings'; -import { createIndicesForQuads, premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; -import bitTwiddle from 'bit-twiddle'; -import BatchBuffer from './BatchBuffer'; -import generateMultiTextureShader from './generateMultiTextureShader'; -import { ENV } from '@pixi/constants'; - -let TICK = 0; -// const TEXTURE_TICK = 0; - -/** - * Renderer dedicated to drawing and batching sprites. - * - * @class - * @private - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class SpriteRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. - */ - constructor(renderer) - { - super(renderer); - - /** - * Number of values sent in the vertex buffer. - * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 - * - * @member {number} - */ - this.vertSize = 5; - - /** - * The size of the vertex information in bytes. - * - * @member {number} - */ - this.vertByteSize = this.vertSize * 4; - - /** - * The number of images in the SpriteRenderer before it flushes. - * - * @member {number} - */ - this.size = settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop - - // the total number of bytes in our batch - // let numVerts = this.size * 4 * this.vertByteSize; - - this.buffers = []; - for (let i = 1; i <= bitTwiddle.nextPow2(this.size); i *= 2) - { - this.buffers.push(new BatchBuffer(i * 4 * this.vertByteSize)); - } - - /** - * Holds the indices of the geometry (quads) to draw - * - * @member {Uint16Array} - */ - this.indices = createIndicesForQuads(this.size); - this.indexBuffer = new Buffer(this.indices, true, true); - - /** - * The default shaders that is used if a sprite doesn't have a more specific one. - * there is a shader for each number of textures that can be rendered. - * These shaders will also be generated on the fly as required. - * @member {PIXI.Shader[]} - */ - this.shader = null; - - this.currentIndex = 0; - this.groups = []; - - for (let k = 0; k < this.size; k++) - { - this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; - } - - this.sprites = []; - - this.vertexBuffers = []; - this.vaos = []; - - this.vaoMax = 2; - this.vertexCount = 0; - - this.renderer.on('prerender', this.onPrerender, this); - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - const gl = this.renderer.gl; - - if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) - { - this.MAX_TEXTURES = 1; - } - else - { - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); - } - - // generate generateMultiTextureProgram, may be a better move? - this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); - - // we use the second shader as the first one depending on your browser may omit aTextureId - // as it is not used by the shader so is optimized out. - for (let i = 0; i < this.vaoMax; i++) - { - const buffer = new Buffer(null, false); - - /* eslint-disable max-len */ - this.vaos[i] = new Geometry() - .addAttribute('aVertexPosition', buffer, 2, false, gl.FLOAT) - .addAttribute('aTextureCoord', buffer, 2, true, gl.UNSIGNED_SHORT) - .addAttribute('aColor', buffer, 4, true, gl.UNSIGNED_BYTE) - .addAttribute('aTextureId', buffer, 1, true, gl.FLOAT) - .addIndex(this.indexBuffer); - /* eslint-enable max-len */ - - this.vertexBuffers[i] = buffer; - } - } - - /** - * Called before the renderer starts rendering. - * - */ - onPrerender() - { - this.vertexCount = 0; - } - - /** - * Renders the sprite object. - * - * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch - */ - render(sprite) - { - // TODO set blend modes.. - // check texture.. - if (this.currentIndex >= this.size) - { - this.flush(); - } - - // get the uvs for the texture - - // if the uvs have not updated then no point rendering just yet! - if (!sprite._texture._uvs) - { - return; - } - - // push a texture. - // increment the batchsize - this.sprites[this.currentIndex++] = sprite; - } - - /** - * Renders the content and empties the current batch. - * - */ - flush() - { - if (this.currentIndex === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - - const np2 = bitTwiddle.nextPow2(this.currentIndex); - const log2 = bitTwiddle.log2(np2); - const buffer = this.buffers[log2]; - - const sprites = this.sprites; - const groups = this.groups; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let nextTexture; - let currentTexture; - let groupCount = 1; - let textureId = 0; - let textureCount = 0; - let currentGroup = groups[0]; - let vertexData; - let uvs; - let blendMode = premultiplyBlendMode[ - sprites[0]._texture.baseTexture.premultiplyAlpha ? 1 : 0][sprites[0].blendMode]; - - currentGroup.textureCount = 0; - currentGroup.start = 0; - currentGroup.blend = blendMode; - - TICK++; - - let i; - - for (i = 0; i < this.currentIndex; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - const sprite = sprites[i]; - - nextTexture = sprite._texture.baseTexture; - textureId = nextTexture._id; - - const spriteBlendMode = premultiplyBlendMode[Number(nextTexture.premultiplyAlpha)][sprite.blendMode]; - - if (blendMode !== spriteBlendMode) - { - blendMode = spriteBlendMode; - - // force the batch to break! - currentTexture = null; - textureCount = MAX_TEXTURES; - TICK++; - } - - if (currentTexture !== nextTexture) - { - currentTexture = nextTexture; - - if (nextTexture._enabled !== TICK) - { - if (textureCount === MAX_TEXTURES) - { - TICK++; - - textureCount = 0; - - currentGroup.size = i - currentGroup.start; - - currentGroup = groups[groupCount++]; - currentGroup.textureCount = 0; - currentGroup.blend = blendMode; - currentGroup.start = i; - } - - nextTexture.touched = touch; - nextTexture._enabled = TICK; - nextTexture._id = textureCount; - - currentGroup.textures[currentGroup.textureCount++] = nextTexture; - textureCount++; - } - } - - vertexData = sprite.vertexData; - - // TODO this sum does not need to be set each frame.. - uvs = sprite._texture._uvs.uvsUint32; - textureId = nextTexture._id; - - if (this.renderer.roundPixels) - { - const resolution = this.renderer.resolution; - - // xy - float32View[index] = ((vertexData[0] * resolution) | 0) / resolution; - float32View[index + 1] = ((vertexData[1] * resolution) | 0) / resolution; - - // xy - float32View[index + 5] = ((vertexData[2] * resolution) | 0) / resolution; - float32View[index + 6] = ((vertexData[3] * resolution) | 0) / resolution; - - // xy - float32View[index + 10] = ((vertexData[4] * resolution) | 0) / resolution; - float32View[index + 11] = ((vertexData[5] * resolution) | 0) / resolution; - - // xy - float32View[index + 15] = ((vertexData[6] * resolution) | 0) / resolution; - float32View[index + 16] = ((vertexData[7] * resolution) | 0) / resolution; - } - else - { - // xy - float32View[index] = vertexData[0]; - float32View[index + 1] = vertexData[1]; - - // xy - float32View[index + 5] = vertexData[2]; - float32View[index + 6] = vertexData[3]; - - // xy - float32View[index + 10] = vertexData[4]; - float32View[index + 11] = vertexData[5]; - - // xy - float32View[index + 15] = vertexData[6]; - float32View[index + 16] = vertexData[7]; - } - - uint32View[index + 2] = uvs[0]; - uint32View[index + 7] = uvs[1]; - uint32View[index + 12] = uvs[2]; - uint32View[index + 17] = uvs[3]; - /* eslint-disable max-len */ - const alpha = Math.min(sprite.worldAlpha, 1.0); - const argb = alpha < 1.0 && nextTexture.premultiplyAlpha ? premultiplyTint(sprite._tintRGB, alpha) - : sprite._tintRGB + (alpha * 255 << 24); - - uint32View[index + 3] = uint32View[index + 8] = uint32View[index + 13] = uint32View[index + 18] = argb; - - float32View[index + 4] = float32View[index + 9] = float32View[index + 14] = float32View[index + 19] = textureId; - /* eslint-enable max-len */ - - index += 20; - } - - currentGroup.size = i - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this.vaoMax <= this.vertexCount) - { - this.vaoMax++; - - const buffer = new Buffer(null, false); - - /* eslint-disable max-len */ - this.vaos[this.vertexCount] = new Geometry() - .addAttribute('aVertexPosition', buffer, 2, false, gl.FLOAT) - .addAttribute('aTextureCoord', buffer, 2, true, gl.UNSIGNED_SHORT) - .addAttribute('aColor', buffer, 4, true, gl.UNSIGNED_BYTE) - .addAttribute('aTextureId', buffer, 1, true, gl.FLOAT) - .addIndex(this.indexBuffer); - /* eslint-enable max-len */ - - this.vertexBuffers[this.vertexCount] = buffer; - } - - this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); - this.renderer.geometry.bind(this.vaos[this.vertexCount]); - - this.vertexCount++; - } - else - { - // lets use the faster option, always use buffer number 0 - this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); - - this.renderer.geometry.updateBuffers(); - } - - // / render the groups.. - for (i = 0; i < groupCount; i++) - { - const group = groups[i]; - const groupTextureCount = group.textureCount; - - for (let j = 0; j < groupTextureCount; j++) - { - this.renderer.texture.bind(group.textures[j], j); - } - - // set the blend mode.. - this.renderer.state.setBlendMode(group.blend); - - gl.drawElements(gl.TRIANGLES, group.size * 6, gl.UNSIGNED_SHORT, group.start * 6 * 2); - } - - // reset elements for the next flush - this.currentIndex = 0; - } - - /** - * Starts a new sprite batch. - */ - start() - { - this.renderer.shader.bind(this.shader); - - if (settings.CAN_UPLOAD_SAME_BUFFER) - { - // bind buffer #0, we don't need others - this.renderer.geometry.bind(this.vaos[this.vertexCount]); - } - } - - /** - * Stops and flushes the current batch. - * - */ - stop() - { - this.flush(); - } - - /** - * Destroys the SpriteRenderer. - * - */ - destroy() - { - for (let i = 0; i < this.vaoMax; i++) - { - if (this.vertexBuffers[i]) - { - this.vertexBuffers[i].destroy(); - } - if (this.vaos[i]) - { - this.vaos[i].destroy(); - } - } - - if (this.indexBuffer) - { - this.indexBuffer.destroy(); - } - - this.renderer.off('prerender', this.onPrerender, this); - - if (this.shader) - { - this.shader.destroy(); - this.shader = null; - } - - this.vertexBuffers = null; - this.vaos = null; - this.indexBuffer = null; - this.indices = null; - - this.sprites = null; - - for (let i = 0; i < this.buffers.length; ++i) - { - this.buffers[i].destroy(); - } - - super.destroy(); - } -} diff --git a/packages/sprite/src/generateMultiTextureShader.js b/packages/sprite/src/generateMultiTextureShader.js deleted file mode 100644 index f2e27be..0000000 --- a/packages/sprite/src/generateMultiTextureShader.js +++ /dev/null @@ -1,69 +0,0 @@ -import { Shader, UniformGroup } from '@pixi/core'; -import vertex from './texture.vert'; - -const fragTemplate = [ - 'varying vec2 vTextureCoord;', - 'varying vec4 vColor;', - 'varying float vTextureId;', - 'uniform sampler2D uSamplers[%count%];', - - 'void main(void){', - 'vec4 color;', - 'float textureId = floor(vTextureId+0.5);', - '%forloop%', - 'gl_FragColor = color * vColor;', - '}', -].join('\n'); - -export default function generateMultiTextureShader(gl, maxTextures) -{ - const sampleValues = new Int32Array(maxTextures); - - for (let i = 0; i < maxTextures; i++) - { - sampleValues[i] = i; - } - - const uniforms = { - default: UniformGroup.from({ uSamplers: sampleValues }, true), - }; - - let fragmentSrc = fragTemplate; - - fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); - fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); - - const shader = Shader.from(vertex, fragmentSrc, uniforms); - - return shader; -} - -function generateSampleSrc(maxTextures) -{ - let src = ''; - - src += '\n'; - src += '\n'; - - for (let i = 0; i < maxTextures; i++) - { - if (i > 0) - { - src += '\nelse '; - } - - if (i < maxTextures - 1) - { - src += `if(textureId == ${i}.0)`; - } - - src += '\n{'; - src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; - src += '\n}'; - } - - src += '\n'; - src += '\n'; - - return src; -} diff --git a/packages/sprite/src/index.js b/packages/sprite/src/index.js index edf477c..c5179d7 100644 --- a/packages/sprite/src/index.js +++ b/packages/sprite/src/index.js @@ -1,2 +1 @@ export { default as Sprite } from './Sprite'; -export { default as SpriteRenderer } from './SpriteRenderer'; diff --git a/packages/sprite/src/texture.vert b/packages/sprite/src/texture.vert deleted file mode 100644 index 18b89ff..0000000 --- a/packages/sprite/src/texture.vert +++ /dev/null @@ -1,19 +0,0 @@ -precision highp float; -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; -attribute vec4 aColor; -attribute float aTextureId; - -uniform mat3 projectionMatrix; - -varying vec2 vTextureCoord; -varying vec4 vColor; -varying float vTextureId; - -void main(void){ - gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = aTextureCoord; - vTextureId = aTextureId; - vColor = aColor; -} diff --git a/packages/sprite/test/SpriteRenderer.js b/packages/sprite/test/SpriteRenderer.js deleted file mode 100644 index 35758ad..0000000 --- a/packages/sprite/test/SpriteRenderer.js +++ /dev/null @@ -1,43 +0,0 @@ -const { SpriteRenderer } = require('../'); - -const mockrunner = { - contextChange: { - remove: () => 1, - add: () => 1, - }, -}; - -describe('SpriteRenderer', function () -{ - it('can be destroyed', function () - { - const destroyable = { destroy: sinon.stub() }; - const webgl = { - on: sinon.stub(), - runners: mockrunner, - off: sinon.stub(), - }; - const renderer = new SpriteRenderer(webgl); - - // simulate onContextChange - renderer.vertexBuffers = [destroyable, destroyable]; - renderer.vaos = [destroyable, destroyable]; - renderer.indexBuffer = destroyable; - renderer.shader = destroyable; - - expect(() => renderer.destroy()).to.not.throw(); - }); - - it('can be destroyed immediately', function () - { - const webgl = { - on: sinon.stub(), - runners: mockrunner, - off: sinon.stub(), - }; - - const renderer = new SpriteRenderer(webgl); - - expect(() => renderer.destroy()).to.not.throw(); - }); -}); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/packages/mesh/src/MeshGeometry.js b/packages/mesh/src/MeshGeometry.js new file mode 100644 index 0000000..ae6c702 --- /dev/null +++ b/packages/mesh/src/MeshGeometry.js @@ -0,0 +1,61 @@ +import { TYPES } from '@pixi/constants'; +import { Buffer, Geometry } from '@pixi/core'; + +/** + * Standard 2D geometry used in PixiJS. + * + * Geometry can be defined without passing in a style or data if required. + * + * ```js + * const geometry = new PIXI.Geometry(); + * + * geometry.addAttribute('positions', [0, 0, 100, 0, 100, 100, 0, 100], 2); + * geometry.addAttribute('uvs', [0,0,1,0,1,1,0,1], 2); + * geometry.addIndex([0,1,2,1,3,2]); + * + * ``` + * @class + * @memberof PIXI + * @extends PIXI.Geometry + */ +export default class MeshGeometry extends Geometry +{ + /** + * @param {Float32Array|number[]} vertices - Positional data on geometry. + * @param {Float32Array|number[]} uvs - Texture UVs. + * @param {Uint16Array|number[]} index - IndexBuffer + */ + constructor(vertices, uvs, index) + { + super(); + + const verticesBuffer = new Buffer(vertices); + const uvsBuffer = new Buffer(uvs, true); + const indexBuffer = new Buffer(index, true, true); + + this.addAttribute('aVertexPosition', verticesBuffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', uvsBuffer, 2, false, TYPES.FLOAT) + .addIndex(indexBuffer); + + /** + * Dirty flag to limit update calls on Mesh. For example, + * limiting updates on a single Mesh instance with a shared Geometry + * within the render loop. + * @private + * @member {number} + * @default -1 + */ + this._updateId = -1; + } + + /** + * If the vertex position is updated. + * @member {number} + * @readonly + * @private + */ + get vertexDirtyId() + { + return this.buffers[0]._updateID; + } +} diff --git a/packages/mesh/src/MeshMaterial.js b/packages/mesh/src/MeshMaterial.js new file mode 100644 index 0000000..028cf49 --- /dev/null +++ b/packages/mesh/src/MeshMaterial.js @@ -0,0 +1,130 @@ +import { Shader, Program, TextureMatrix } from '@pixi/core'; +import vertex from './shader/mesh.vert'; +import fragment from './shader/mesh.frag'; +import { Matrix } from '@pixi/math'; +import { premultiplyTintToRgba } from '@pixi/utils'; + +/** + * Slightly opinionated default shader for PixiJS 2D objects. + * @class + * @memberof PIXI + * @extends PIXI.Shader + */ +export default class MeshMaterial extends Shader +{ + /** + * @param {PIXI.Texture} uSampler - Texture that material uses to render. + * @param {object} [options] - Additional options + * @param {number} [options.alpha=1] - Default alpha. + * @param {number} [options.tint=0xFFFFFF] - Default tint. + */ + constructor(uSampler, options) + { + const program = Program.from(vertex, fragment); + + const uniforms = { + uSampler, + alpha: 1, + uTextureMatrix: Matrix.IDENTITY, + uColor: new Float32Array([1, 1, 1, 1]), + }; + + super(program, uniforms); + + /** + * Only do update if tint or alpha changes. + * @member {boolean} + * @private + * @default false + */ + this._colorDirty = false; + + /** + * TextureMatrix instance for this Mesh, used to track Texture changes + * + * @member {PIXI.TextureMatrix} + * @readonly + */ + // TODO get this back in! + this.uvMatrix = new TextureMatrix(this.uSampler); + + /** + * `true` if shader can be batch with the renderer's batch system. + * @member {boolean} + * @default true + */ + this.batchable = true; + + // Set defaults + const { tint, alpha } = Object.assign({ + tint: 0xFFFFFF, + alpha: 1, + }, options); + + this.tint = tint; + this.alpha = alpha; + } + + /** + * Reference to the texture being rendered. + * @member {PIXI.Texture} + */ + get texture() + { + return this.uniforms.uSampler; + } + set texture(value) + { + this.uniforms.uSampler = value; + } + + /** + * This gets automatically set by the object using this. + * @default 1 + * @member {number} + */ + set alpha(value) + { + if (value === this._alpha) return; + + this._alpha = value; + this._colorDirty = true; + } + get alpha() + { + return this._alpha; + } + + /** + * Multiply tint for the material. + * @member {number} + * @default 0xFFFFFF + */ + set tint(value) + { + if (value === this._tint) return; + + this._tint = value; + this._tintRGB = (value >> 16) + (value & 0xff00) + ((value & 0xff) << 16); + this._colorDirty = true; + } + get tint() + { + return this._tint; + } + + /** + * Gets called automatically by the Mesh. Intended to be overridden for custom + * MeshMaterial objects. + */ + update() + { + if (this._colorDirty) + { + this._colorDirty = false; + const baseTexture = this.texture.baseTexture; + + premultiplyTintToRgba(this._tintRGB, this._alpha, this.uniforms.uColor, baseTexture.premultiplyAlpha); + } + } +} diff --git a/packages/mesh/src/MeshRenderer.js b/packages/mesh/src/MeshRenderer.js deleted file mode 100644 index d7baf9b..0000000 --- a/packages/mesh/src/MeshRenderer.js +++ /dev/null @@ -1,77 +0,0 @@ -import { ObjectRenderer } from '@pixi/core'; -import { Matrix } from '@pixi/math'; - -/** - * WebGL renderer plugin for tiling sprites - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class MeshRenderer extends ObjectRenderer -{ - /** - * constructor for renderer - * - * @param {Renderer} renderer The renderer this tiling awesomeness works for. - */ - constructor(renderer) - { - super(renderer); - - this.shader = null; - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * renders mesh - * @private - * @param {PIXI.Mesh} mesh mesh instance - */ - render(mesh) - { - // bind the shader.. - - // TODO - // set the shader props.. - // probably only need to set once! - // as its then a reference.. - if (mesh.shader.program.uniformData.translationMatrix) - { - // the transform! - mesh.shader.uniforms.translationMatrix = mesh.transform.worldTransform.toArray(true); - } - if (mesh.shader.uniforms.uTextureMatrix) - { - if (mesh.uploadUvMatrix) - { - mesh.shader.uniforms.uTextureMatrix = mesh.uvMatrix.mapCoord.toArray(true); - } - else - { - mesh.shader.uniforms.uTextureMatrix = Matrix.IDENTITY.toArray(true); - } - } - - // bind and sync uniforms.. - this.renderer.shader.bind(mesh.shader); - - // set state.. - this.renderer.state.setState(mesh.state); - - // bind the geometry... - this.renderer.geometry.bind(mesh.geometry, mesh.shader); - // then render it - this.renderer.geometry.draw(mesh.drawMode, mesh.size, mesh.start, mesh.geometry.instanceCount); - } -} diff --git a/packages/mesh/src/index.js b/packages/mesh/src/index.js index e14ab9f..9e6dcd8 100644 --- a/packages/mesh/src/index.js +++ b/packages/mesh/src/index.js @@ -1,2 +1,4 @@ export { default as Mesh } from './Mesh'; -export { default as MeshRenderer } from './MeshRenderer'; +export { default as MeshMaterial } from './MeshMaterial'; +export { default as MeshGeometry } from './MeshGeometry'; + diff --git a/packages/mesh/src/shader/mesh.frag b/packages/mesh/src/shader/mesh.frag new file mode 100644 index 0000000..6096983 --- /dev/null +++ b/packages/mesh/src/shader/mesh.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +uniform vec4 uColor; + +uniform sampler2D uSampler; + +void main(void) +{ + gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; +} diff --git a/packages/mesh/src/shader/mesh.vert b/packages/mesh/src/shader/mesh.vert new file mode 100644 index 0000000..0526df9 --- /dev/null +++ b/packages/mesh/src/shader/mesh.vert @@ -0,0 +1,15 @@ +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTextureMatrix; + +varying vec2 vTextureCoord; + +void main(void) +{ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; +} diff --git a/packages/sprite/README.md b/packages/sprite/README.md index 3783526..1972ceb 100644 --- a/packages/sprite/README.md +++ b/packages/sprite/README.md @@ -9,8 +9,7 @@ ## Usage ```js -import { SpriteRenderer } from '@pixi/sprite'; -import { Renderer } from '@pixi/core'; +import { Sprite } from '@pixi/sprite'; -Renderer.registerPlugin('sprite', SpriteRenderer); +const sprite = new Sprite(); ``` \ No newline at end of file diff --git a/packages/sprite/src/BatchBuffer.js b/packages/sprite/src/BatchBuffer.js deleted file mode 100644 index cbc4c72..0000000 --- a/packages/sprite/src/BatchBuffer.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.positions = null; - this.uvs = null; - this.colors = null; - } -} diff --git a/packages/sprite/src/Sprite.js b/packages/sprite/src/Sprite.js index 91546cf..7b66e6e 100644 --- a/packages/sprite/src/Sprite.js +++ b/packages/sprite/src/Sprite.js @@ -5,10 +5,11 @@ import { Container } from '@pixi/display'; const tempPoint = new Point(); +const indices = new Uint16Array([0, 1, 2, 0, 2, 3]); /** * The Sprite object is the base for all textured objects that are rendered to the screen - * +* * A sprite can be created directly from an image like this: * * ```js @@ -119,6 +120,8 @@ */ this.cachedTint = 0xFFFFFF; + this.uvs = null; + // call texture setter this.texture = texture || Texture.EMPTY; @@ -144,6 +147,12 @@ this._transformTrimmedID = -1; this._textureTrimmedID = -1; + // Batchable stuff.. + // TODO could make this a mixin? + this.indices = indices; + this.size = 4; + this.start = 0; + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. @@ -151,7 +160,12 @@ * @member {string} * @default 'sprite' */ - this.pluginName = 'sprite'; + this.pluginName = 'batch'; + + /** + * used to fast check if a sprite is.. a sprite! + */ + this.isSprite = true; } /** @@ -165,6 +179,7 @@ this._textureTrimmedID = -1; this.cachedTint = 0xFFFFFF; + this.uvs = this._texture._uvs.uvsFloat32; // so if _width is 0 then width was not set.. if (this._width) { diff --git a/packages/sprite/src/SpriteRenderer.js b/packages/sprite/src/SpriteRenderer.js deleted file mode 100644 index 136cdcc..0000000 --- a/packages/sprite/src/SpriteRenderer.js +++ /dev/null @@ -1,463 +0,0 @@ -import { Geometry, - Buffer, - ObjectRenderer, - checkMaxIfStatementsInShader } from '@pixi/core'; -import { settings } from '@pixi/settings'; -import { createIndicesForQuads, premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; -import bitTwiddle from 'bit-twiddle'; -import BatchBuffer from './BatchBuffer'; -import generateMultiTextureShader from './generateMultiTextureShader'; -import { ENV } from '@pixi/constants'; - -let TICK = 0; -// const TEXTURE_TICK = 0; - -/** - * Renderer dedicated to drawing and batching sprites. - * - * @class - * @private - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class SpriteRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. - */ - constructor(renderer) - { - super(renderer); - - /** - * Number of values sent in the vertex buffer. - * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 - * - * @member {number} - */ - this.vertSize = 5; - - /** - * The size of the vertex information in bytes. - * - * @member {number} - */ - this.vertByteSize = this.vertSize * 4; - - /** - * The number of images in the SpriteRenderer before it flushes. - * - * @member {number} - */ - this.size = settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop - - // the total number of bytes in our batch - // let numVerts = this.size * 4 * this.vertByteSize; - - this.buffers = []; - for (let i = 1; i <= bitTwiddle.nextPow2(this.size); i *= 2) - { - this.buffers.push(new BatchBuffer(i * 4 * this.vertByteSize)); - } - - /** - * Holds the indices of the geometry (quads) to draw - * - * @member {Uint16Array} - */ - this.indices = createIndicesForQuads(this.size); - this.indexBuffer = new Buffer(this.indices, true, true); - - /** - * The default shaders that is used if a sprite doesn't have a more specific one. - * there is a shader for each number of textures that can be rendered. - * These shaders will also be generated on the fly as required. - * @member {PIXI.Shader[]} - */ - this.shader = null; - - this.currentIndex = 0; - this.groups = []; - - for (let k = 0; k < this.size; k++) - { - this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; - } - - this.sprites = []; - - this.vertexBuffers = []; - this.vaos = []; - - this.vaoMax = 2; - this.vertexCount = 0; - - this.renderer.on('prerender', this.onPrerender, this); - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - const gl = this.renderer.gl; - - if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) - { - this.MAX_TEXTURES = 1; - } - else - { - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); - } - - // generate generateMultiTextureProgram, may be a better move? - this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); - - // we use the second shader as the first one depending on your browser may omit aTextureId - // as it is not used by the shader so is optimized out. - for (let i = 0; i < this.vaoMax; i++) - { - const buffer = new Buffer(null, false); - - /* eslint-disable max-len */ - this.vaos[i] = new Geometry() - .addAttribute('aVertexPosition', buffer, 2, false, gl.FLOAT) - .addAttribute('aTextureCoord', buffer, 2, true, gl.UNSIGNED_SHORT) - .addAttribute('aColor', buffer, 4, true, gl.UNSIGNED_BYTE) - .addAttribute('aTextureId', buffer, 1, true, gl.FLOAT) - .addIndex(this.indexBuffer); - /* eslint-enable max-len */ - - this.vertexBuffers[i] = buffer; - } - } - - /** - * Called before the renderer starts rendering. - * - */ - onPrerender() - { - this.vertexCount = 0; - } - - /** - * Renders the sprite object. - * - * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch - */ - render(sprite) - { - // TODO set blend modes.. - // check texture.. - if (this.currentIndex >= this.size) - { - this.flush(); - } - - // get the uvs for the texture - - // if the uvs have not updated then no point rendering just yet! - if (!sprite._texture._uvs) - { - return; - } - - // push a texture. - // increment the batchsize - this.sprites[this.currentIndex++] = sprite; - } - - /** - * Renders the content and empties the current batch. - * - */ - flush() - { - if (this.currentIndex === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - - const np2 = bitTwiddle.nextPow2(this.currentIndex); - const log2 = bitTwiddle.log2(np2); - const buffer = this.buffers[log2]; - - const sprites = this.sprites; - const groups = this.groups; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let nextTexture; - let currentTexture; - let groupCount = 1; - let textureId = 0; - let textureCount = 0; - let currentGroup = groups[0]; - let vertexData; - let uvs; - let blendMode = premultiplyBlendMode[ - sprites[0]._texture.baseTexture.premultiplyAlpha ? 1 : 0][sprites[0].blendMode]; - - currentGroup.textureCount = 0; - currentGroup.start = 0; - currentGroup.blend = blendMode; - - TICK++; - - let i; - - for (i = 0; i < this.currentIndex; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - const sprite = sprites[i]; - - nextTexture = sprite._texture.baseTexture; - textureId = nextTexture._id; - - const spriteBlendMode = premultiplyBlendMode[Number(nextTexture.premultiplyAlpha)][sprite.blendMode]; - - if (blendMode !== spriteBlendMode) - { - blendMode = spriteBlendMode; - - // force the batch to break! - currentTexture = null; - textureCount = MAX_TEXTURES; - TICK++; - } - - if (currentTexture !== nextTexture) - { - currentTexture = nextTexture; - - if (nextTexture._enabled !== TICK) - { - if (textureCount === MAX_TEXTURES) - { - TICK++; - - textureCount = 0; - - currentGroup.size = i - currentGroup.start; - - currentGroup = groups[groupCount++]; - currentGroup.textureCount = 0; - currentGroup.blend = blendMode; - currentGroup.start = i; - } - - nextTexture.touched = touch; - nextTexture._enabled = TICK; - nextTexture._id = textureCount; - - currentGroup.textures[currentGroup.textureCount++] = nextTexture; - textureCount++; - } - } - - vertexData = sprite.vertexData; - - // TODO this sum does not need to be set each frame.. - uvs = sprite._texture._uvs.uvsUint32; - textureId = nextTexture._id; - - if (this.renderer.roundPixels) - { - const resolution = this.renderer.resolution; - - // xy - float32View[index] = ((vertexData[0] * resolution) | 0) / resolution; - float32View[index + 1] = ((vertexData[1] * resolution) | 0) / resolution; - - // xy - float32View[index + 5] = ((vertexData[2] * resolution) | 0) / resolution; - float32View[index + 6] = ((vertexData[3] * resolution) | 0) / resolution; - - // xy - float32View[index + 10] = ((vertexData[4] * resolution) | 0) / resolution; - float32View[index + 11] = ((vertexData[5] * resolution) | 0) / resolution; - - // xy - float32View[index + 15] = ((vertexData[6] * resolution) | 0) / resolution; - float32View[index + 16] = ((vertexData[7] * resolution) | 0) / resolution; - } - else - { - // xy - float32View[index] = vertexData[0]; - float32View[index + 1] = vertexData[1]; - - // xy - float32View[index + 5] = vertexData[2]; - float32View[index + 6] = vertexData[3]; - - // xy - float32View[index + 10] = vertexData[4]; - float32View[index + 11] = vertexData[5]; - - // xy - float32View[index + 15] = vertexData[6]; - float32View[index + 16] = vertexData[7]; - } - - uint32View[index + 2] = uvs[0]; - uint32View[index + 7] = uvs[1]; - uint32View[index + 12] = uvs[2]; - uint32View[index + 17] = uvs[3]; - /* eslint-disable max-len */ - const alpha = Math.min(sprite.worldAlpha, 1.0); - const argb = alpha < 1.0 && nextTexture.premultiplyAlpha ? premultiplyTint(sprite._tintRGB, alpha) - : sprite._tintRGB + (alpha * 255 << 24); - - uint32View[index + 3] = uint32View[index + 8] = uint32View[index + 13] = uint32View[index + 18] = argb; - - float32View[index + 4] = float32View[index + 9] = float32View[index + 14] = float32View[index + 19] = textureId; - /* eslint-enable max-len */ - - index += 20; - } - - currentGroup.size = i - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this.vaoMax <= this.vertexCount) - { - this.vaoMax++; - - const buffer = new Buffer(null, false); - - /* eslint-disable max-len */ - this.vaos[this.vertexCount] = new Geometry() - .addAttribute('aVertexPosition', buffer, 2, false, gl.FLOAT) - .addAttribute('aTextureCoord', buffer, 2, true, gl.UNSIGNED_SHORT) - .addAttribute('aColor', buffer, 4, true, gl.UNSIGNED_BYTE) - .addAttribute('aTextureId', buffer, 1, true, gl.FLOAT) - .addIndex(this.indexBuffer); - /* eslint-enable max-len */ - - this.vertexBuffers[this.vertexCount] = buffer; - } - - this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); - this.renderer.geometry.bind(this.vaos[this.vertexCount]); - - this.vertexCount++; - } - else - { - // lets use the faster option, always use buffer number 0 - this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); - - this.renderer.geometry.updateBuffers(); - } - - // / render the groups.. - for (i = 0; i < groupCount; i++) - { - const group = groups[i]; - const groupTextureCount = group.textureCount; - - for (let j = 0; j < groupTextureCount; j++) - { - this.renderer.texture.bind(group.textures[j], j); - } - - // set the blend mode.. - this.renderer.state.setBlendMode(group.blend); - - gl.drawElements(gl.TRIANGLES, group.size * 6, gl.UNSIGNED_SHORT, group.start * 6 * 2); - } - - // reset elements for the next flush - this.currentIndex = 0; - } - - /** - * Starts a new sprite batch. - */ - start() - { - this.renderer.shader.bind(this.shader); - - if (settings.CAN_UPLOAD_SAME_BUFFER) - { - // bind buffer #0, we don't need others - this.renderer.geometry.bind(this.vaos[this.vertexCount]); - } - } - - /** - * Stops and flushes the current batch. - * - */ - stop() - { - this.flush(); - } - - /** - * Destroys the SpriteRenderer. - * - */ - destroy() - { - for (let i = 0; i < this.vaoMax; i++) - { - if (this.vertexBuffers[i]) - { - this.vertexBuffers[i].destroy(); - } - if (this.vaos[i]) - { - this.vaos[i].destroy(); - } - } - - if (this.indexBuffer) - { - this.indexBuffer.destroy(); - } - - this.renderer.off('prerender', this.onPrerender, this); - - if (this.shader) - { - this.shader.destroy(); - this.shader = null; - } - - this.vertexBuffers = null; - this.vaos = null; - this.indexBuffer = null; - this.indices = null; - - this.sprites = null; - - for (let i = 0; i < this.buffers.length; ++i) - { - this.buffers[i].destroy(); - } - - super.destroy(); - } -} diff --git a/packages/sprite/src/generateMultiTextureShader.js b/packages/sprite/src/generateMultiTextureShader.js deleted file mode 100644 index f2e27be..0000000 --- a/packages/sprite/src/generateMultiTextureShader.js +++ /dev/null @@ -1,69 +0,0 @@ -import { Shader, UniformGroup } from '@pixi/core'; -import vertex from './texture.vert'; - -const fragTemplate = [ - 'varying vec2 vTextureCoord;', - 'varying vec4 vColor;', - 'varying float vTextureId;', - 'uniform sampler2D uSamplers[%count%];', - - 'void main(void){', - 'vec4 color;', - 'float textureId = floor(vTextureId+0.5);', - '%forloop%', - 'gl_FragColor = color * vColor;', - '}', -].join('\n'); - -export default function generateMultiTextureShader(gl, maxTextures) -{ - const sampleValues = new Int32Array(maxTextures); - - for (let i = 0; i < maxTextures; i++) - { - sampleValues[i] = i; - } - - const uniforms = { - default: UniformGroup.from({ uSamplers: sampleValues }, true), - }; - - let fragmentSrc = fragTemplate; - - fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); - fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); - - const shader = Shader.from(vertex, fragmentSrc, uniforms); - - return shader; -} - -function generateSampleSrc(maxTextures) -{ - let src = ''; - - src += '\n'; - src += '\n'; - - for (let i = 0; i < maxTextures; i++) - { - if (i > 0) - { - src += '\nelse '; - } - - if (i < maxTextures - 1) - { - src += `if(textureId == ${i}.0)`; - } - - src += '\n{'; - src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; - src += '\n}'; - } - - src += '\n'; - src += '\n'; - - return src; -} diff --git a/packages/sprite/src/index.js b/packages/sprite/src/index.js index edf477c..c5179d7 100644 --- a/packages/sprite/src/index.js +++ b/packages/sprite/src/index.js @@ -1,2 +1 @@ export { default as Sprite } from './Sprite'; -export { default as SpriteRenderer } from './SpriteRenderer'; diff --git a/packages/sprite/src/texture.vert b/packages/sprite/src/texture.vert deleted file mode 100644 index 18b89ff..0000000 --- a/packages/sprite/src/texture.vert +++ /dev/null @@ -1,19 +0,0 @@ -precision highp float; -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; -attribute vec4 aColor; -attribute float aTextureId; - -uniform mat3 projectionMatrix; - -varying vec2 vTextureCoord; -varying vec4 vColor; -varying float vTextureId; - -void main(void){ - gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = aTextureCoord; - vTextureId = aTextureId; - vColor = aColor; -} diff --git a/packages/sprite/test/SpriteRenderer.js b/packages/sprite/test/SpriteRenderer.js deleted file mode 100644 index 35758ad..0000000 --- a/packages/sprite/test/SpriteRenderer.js +++ /dev/null @@ -1,43 +0,0 @@ -const { SpriteRenderer } = require('../'); - -const mockrunner = { - contextChange: { - remove: () => 1, - add: () => 1, - }, -}; - -describe('SpriteRenderer', function () -{ - it('can be destroyed', function () - { - const destroyable = { destroy: sinon.stub() }; - const webgl = { - on: sinon.stub(), - runners: mockrunner, - off: sinon.stub(), - }; - const renderer = new SpriteRenderer(webgl); - - // simulate onContextChange - renderer.vertexBuffers = [destroyable, destroyable]; - renderer.vaos = [destroyable, destroyable]; - renderer.indexBuffer = destroyable; - renderer.shader = destroyable; - - expect(() => renderer.destroy()).to.not.throw(); - }); - - it('can be destroyed immediately', function () - { - const webgl = { - on: sinon.stub(), - runners: mockrunner, - off: sinon.stub(), - }; - - const renderer = new SpriteRenderer(webgl); - - expect(() => renderer.destroy()).to.not.throw(); - }); -}); diff --git a/packages/sprite/test/index.js b/packages/sprite/test/index.js index 76e90bc..b5ea071 100644 --- a/packages/sprite/test/index.js +++ b/packages/sprite/test/index.js @@ -1,2 +1 @@ require('./Sprite'); -require('./SpriteRenderer'); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/packages/mesh/src/MeshGeometry.js b/packages/mesh/src/MeshGeometry.js new file mode 100644 index 0000000..ae6c702 --- /dev/null +++ b/packages/mesh/src/MeshGeometry.js @@ -0,0 +1,61 @@ +import { TYPES } from '@pixi/constants'; +import { Buffer, Geometry } from '@pixi/core'; + +/** + * Standard 2D geometry used in PixiJS. + * + * Geometry can be defined without passing in a style or data if required. + * + * ```js + * const geometry = new PIXI.Geometry(); + * + * geometry.addAttribute('positions', [0, 0, 100, 0, 100, 100, 0, 100], 2); + * geometry.addAttribute('uvs', [0,0,1,0,1,1,0,1], 2); + * geometry.addIndex([0,1,2,1,3,2]); + * + * ``` + * @class + * @memberof PIXI + * @extends PIXI.Geometry + */ +export default class MeshGeometry extends Geometry +{ + /** + * @param {Float32Array|number[]} vertices - Positional data on geometry. + * @param {Float32Array|number[]} uvs - Texture UVs. + * @param {Uint16Array|number[]} index - IndexBuffer + */ + constructor(vertices, uvs, index) + { + super(); + + const verticesBuffer = new Buffer(vertices); + const uvsBuffer = new Buffer(uvs, true); + const indexBuffer = new Buffer(index, true, true); + + this.addAttribute('aVertexPosition', verticesBuffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', uvsBuffer, 2, false, TYPES.FLOAT) + .addIndex(indexBuffer); + + /** + * Dirty flag to limit update calls on Mesh. For example, + * limiting updates on a single Mesh instance with a shared Geometry + * within the render loop. + * @private + * @member {number} + * @default -1 + */ + this._updateId = -1; + } + + /** + * If the vertex position is updated. + * @member {number} + * @readonly + * @private + */ + get vertexDirtyId() + { + return this.buffers[0]._updateID; + } +} diff --git a/packages/mesh/src/MeshMaterial.js b/packages/mesh/src/MeshMaterial.js new file mode 100644 index 0000000..028cf49 --- /dev/null +++ b/packages/mesh/src/MeshMaterial.js @@ -0,0 +1,130 @@ +import { Shader, Program, TextureMatrix } from '@pixi/core'; +import vertex from './shader/mesh.vert'; +import fragment from './shader/mesh.frag'; +import { Matrix } from '@pixi/math'; +import { premultiplyTintToRgba } from '@pixi/utils'; + +/** + * Slightly opinionated default shader for PixiJS 2D objects. + * @class + * @memberof PIXI + * @extends PIXI.Shader + */ +export default class MeshMaterial extends Shader +{ + /** + * @param {PIXI.Texture} uSampler - Texture that material uses to render. + * @param {object} [options] - Additional options + * @param {number} [options.alpha=1] - Default alpha. + * @param {number} [options.tint=0xFFFFFF] - Default tint. + */ + constructor(uSampler, options) + { + const program = Program.from(vertex, fragment); + + const uniforms = { + uSampler, + alpha: 1, + uTextureMatrix: Matrix.IDENTITY, + uColor: new Float32Array([1, 1, 1, 1]), + }; + + super(program, uniforms); + + /** + * Only do update if tint or alpha changes. + * @member {boolean} + * @private + * @default false + */ + this._colorDirty = false; + + /** + * TextureMatrix instance for this Mesh, used to track Texture changes + * + * @member {PIXI.TextureMatrix} + * @readonly + */ + // TODO get this back in! + this.uvMatrix = new TextureMatrix(this.uSampler); + + /** + * `true` if shader can be batch with the renderer's batch system. + * @member {boolean} + * @default true + */ + this.batchable = true; + + // Set defaults + const { tint, alpha } = Object.assign({ + tint: 0xFFFFFF, + alpha: 1, + }, options); + + this.tint = tint; + this.alpha = alpha; + } + + /** + * Reference to the texture being rendered. + * @member {PIXI.Texture} + */ + get texture() + { + return this.uniforms.uSampler; + } + set texture(value) + { + this.uniforms.uSampler = value; + } + + /** + * This gets automatically set by the object using this. + * @default 1 + * @member {number} + */ + set alpha(value) + { + if (value === this._alpha) return; + + this._alpha = value; + this._colorDirty = true; + } + get alpha() + { + return this._alpha; + } + + /** + * Multiply tint for the material. + * @member {number} + * @default 0xFFFFFF + */ + set tint(value) + { + if (value === this._tint) return; + + this._tint = value; + this._tintRGB = (value >> 16) + (value & 0xff00) + ((value & 0xff) << 16); + this._colorDirty = true; + } + get tint() + { + return this._tint; + } + + /** + * Gets called automatically by the Mesh. Intended to be overridden for custom + * MeshMaterial objects. + */ + update() + { + if (this._colorDirty) + { + this._colorDirty = false; + const baseTexture = this.texture.baseTexture; + + premultiplyTintToRgba(this._tintRGB, this._alpha, this.uniforms.uColor, baseTexture.premultiplyAlpha); + } + } +} diff --git a/packages/mesh/src/MeshRenderer.js b/packages/mesh/src/MeshRenderer.js deleted file mode 100644 index d7baf9b..0000000 --- a/packages/mesh/src/MeshRenderer.js +++ /dev/null @@ -1,77 +0,0 @@ -import { ObjectRenderer } from '@pixi/core'; -import { Matrix } from '@pixi/math'; - -/** - * WebGL renderer plugin for tiling sprites - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class MeshRenderer extends ObjectRenderer -{ - /** - * constructor for renderer - * - * @param {Renderer} renderer The renderer this tiling awesomeness works for. - */ - constructor(renderer) - { - super(renderer); - - this.shader = null; - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * renders mesh - * @private - * @param {PIXI.Mesh} mesh mesh instance - */ - render(mesh) - { - // bind the shader.. - - // TODO - // set the shader props.. - // probably only need to set once! - // as its then a reference.. - if (mesh.shader.program.uniformData.translationMatrix) - { - // the transform! - mesh.shader.uniforms.translationMatrix = mesh.transform.worldTransform.toArray(true); - } - if (mesh.shader.uniforms.uTextureMatrix) - { - if (mesh.uploadUvMatrix) - { - mesh.shader.uniforms.uTextureMatrix = mesh.uvMatrix.mapCoord.toArray(true); - } - else - { - mesh.shader.uniforms.uTextureMatrix = Matrix.IDENTITY.toArray(true); - } - } - - // bind and sync uniforms.. - this.renderer.shader.bind(mesh.shader); - - // set state.. - this.renderer.state.setState(mesh.state); - - // bind the geometry... - this.renderer.geometry.bind(mesh.geometry, mesh.shader); - // then render it - this.renderer.geometry.draw(mesh.drawMode, mesh.size, mesh.start, mesh.geometry.instanceCount); - } -} diff --git a/packages/mesh/src/index.js b/packages/mesh/src/index.js index e14ab9f..9e6dcd8 100644 --- a/packages/mesh/src/index.js +++ b/packages/mesh/src/index.js @@ -1,2 +1,4 @@ export { default as Mesh } from './Mesh'; -export { default as MeshRenderer } from './MeshRenderer'; +export { default as MeshMaterial } from './MeshMaterial'; +export { default as MeshGeometry } from './MeshGeometry'; + diff --git a/packages/mesh/src/shader/mesh.frag b/packages/mesh/src/shader/mesh.frag new file mode 100644 index 0000000..6096983 --- /dev/null +++ b/packages/mesh/src/shader/mesh.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +uniform vec4 uColor; + +uniform sampler2D uSampler; + +void main(void) +{ + gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; +} diff --git a/packages/mesh/src/shader/mesh.vert b/packages/mesh/src/shader/mesh.vert new file mode 100644 index 0000000..0526df9 --- /dev/null +++ b/packages/mesh/src/shader/mesh.vert @@ -0,0 +1,15 @@ +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTextureMatrix; + +varying vec2 vTextureCoord; + +void main(void) +{ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; +} diff --git a/packages/sprite/README.md b/packages/sprite/README.md index 3783526..1972ceb 100644 --- a/packages/sprite/README.md +++ b/packages/sprite/README.md @@ -9,8 +9,7 @@ ## Usage ```js -import { SpriteRenderer } from '@pixi/sprite'; -import { Renderer } from '@pixi/core'; +import { Sprite } from '@pixi/sprite'; -Renderer.registerPlugin('sprite', SpriteRenderer); +const sprite = new Sprite(); ``` \ No newline at end of file diff --git a/packages/sprite/src/BatchBuffer.js b/packages/sprite/src/BatchBuffer.js deleted file mode 100644 index cbc4c72..0000000 --- a/packages/sprite/src/BatchBuffer.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.positions = null; - this.uvs = null; - this.colors = null; - } -} diff --git a/packages/sprite/src/Sprite.js b/packages/sprite/src/Sprite.js index 91546cf..7b66e6e 100644 --- a/packages/sprite/src/Sprite.js +++ b/packages/sprite/src/Sprite.js @@ -5,10 +5,11 @@ import { Container } from '@pixi/display'; const tempPoint = new Point(); +const indices = new Uint16Array([0, 1, 2, 0, 2, 3]); /** * The Sprite object is the base for all textured objects that are rendered to the screen - * +* * A sprite can be created directly from an image like this: * * ```js @@ -119,6 +120,8 @@ */ this.cachedTint = 0xFFFFFF; + this.uvs = null; + // call texture setter this.texture = texture || Texture.EMPTY; @@ -144,6 +147,12 @@ this._transformTrimmedID = -1; this._textureTrimmedID = -1; + // Batchable stuff.. + // TODO could make this a mixin? + this.indices = indices; + this.size = 4; + this.start = 0; + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. @@ -151,7 +160,12 @@ * @member {string} * @default 'sprite' */ - this.pluginName = 'sprite'; + this.pluginName = 'batch'; + + /** + * used to fast check if a sprite is.. a sprite! + */ + this.isSprite = true; } /** @@ -165,6 +179,7 @@ this._textureTrimmedID = -1; this.cachedTint = 0xFFFFFF; + this.uvs = this._texture._uvs.uvsFloat32; // so if _width is 0 then width was not set.. if (this._width) { diff --git a/packages/sprite/src/SpriteRenderer.js b/packages/sprite/src/SpriteRenderer.js deleted file mode 100644 index 136cdcc..0000000 --- a/packages/sprite/src/SpriteRenderer.js +++ /dev/null @@ -1,463 +0,0 @@ -import { Geometry, - Buffer, - ObjectRenderer, - checkMaxIfStatementsInShader } from '@pixi/core'; -import { settings } from '@pixi/settings'; -import { createIndicesForQuads, premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; -import bitTwiddle from 'bit-twiddle'; -import BatchBuffer from './BatchBuffer'; -import generateMultiTextureShader from './generateMultiTextureShader'; -import { ENV } from '@pixi/constants'; - -let TICK = 0; -// const TEXTURE_TICK = 0; - -/** - * Renderer dedicated to drawing and batching sprites. - * - * @class - * @private - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class SpriteRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. - */ - constructor(renderer) - { - super(renderer); - - /** - * Number of values sent in the vertex buffer. - * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 - * - * @member {number} - */ - this.vertSize = 5; - - /** - * The size of the vertex information in bytes. - * - * @member {number} - */ - this.vertByteSize = this.vertSize * 4; - - /** - * The number of images in the SpriteRenderer before it flushes. - * - * @member {number} - */ - this.size = settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop - - // the total number of bytes in our batch - // let numVerts = this.size * 4 * this.vertByteSize; - - this.buffers = []; - for (let i = 1; i <= bitTwiddle.nextPow2(this.size); i *= 2) - { - this.buffers.push(new BatchBuffer(i * 4 * this.vertByteSize)); - } - - /** - * Holds the indices of the geometry (quads) to draw - * - * @member {Uint16Array} - */ - this.indices = createIndicesForQuads(this.size); - this.indexBuffer = new Buffer(this.indices, true, true); - - /** - * The default shaders that is used if a sprite doesn't have a more specific one. - * there is a shader for each number of textures that can be rendered. - * These shaders will also be generated on the fly as required. - * @member {PIXI.Shader[]} - */ - this.shader = null; - - this.currentIndex = 0; - this.groups = []; - - for (let k = 0; k < this.size; k++) - { - this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; - } - - this.sprites = []; - - this.vertexBuffers = []; - this.vaos = []; - - this.vaoMax = 2; - this.vertexCount = 0; - - this.renderer.on('prerender', this.onPrerender, this); - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - const gl = this.renderer.gl; - - if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) - { - this.MAX_TEXTURES = 1; - } - else - { - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); - } - - // generate generateMultiTextureProgram, may be a better move? - this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); - - // we use the second shader as the first one depending on your browser may omit aTextureId - // as it is not used by the shader so is optimized out. - for (let i = 0; i < this.vaoMax; i++) - { - const buffer = new Buffer(null, false); - - /* eslint-disable max-len */ - this.vaos[i] = new Geometry() - .addAttribute('aVertexPosition', buffer, 2, false, gl.FLOAT) - .addAttribute('aTextureCoord', buffer, 2, true, gl.UNSIGNED_SHORT) - .addAttribute('aColor', buffer, 4, true, gl.UNSIGNED_BYTE) - .addAttribute('aTextureId', buffer, 1, true, gl.FLOAT) - .addIndex(this.indexBuffer); - /* eslint-enable max-len */ - - this.vertexBuffers[i] = buffer; - } - } - - /** - * Called before the renderer starts rendering. - * - */ - onPrerender() - { - this.vertexCount = 0; - } - - /** - * Renders the sprite object. - * - * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch - */ - render(sprite) - { - // TODO set blend modes.. - // check texture.. - if (this.currentIndex >= this.size) - { - this.flush(); - } - - // get the uvs for the texture - - // if the uvs have not updated then no point rendering just yet! - if (!sprite._texture._uvs) - { - return; - } - - // push a texture. - // increment the batchsize - this.sprites[this.currentIndex++] = sprite; - } - - /** - * Renders the content and empties the current batch. - * - */ - flush() - { - if (this.currentIndex === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - - const np2 = bitTwiddle.nextPow2(this.currentIndex); - const log2 = bitTwiddle.log2(np2); - const buffer = this.buffers[log2]; - - const sprites = this.sprites; - const groups = this.groups; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let nextTexture; - let currentTexture; - let groupCount = 1; - let textureId = 0; - let textureCount = 0; - let currentGroup = groups[0]; - let vertexData; - let uvs; - let blendMode = premultiplyBlendMode[ - sprites[0]._texture.baseTexture.premultiplyAlpha ? 1 : 0][sprites[0].blendMode]; - - currentGroup.textureCount = 0; - currentGroup.start = 0; - currentGroup.blend = blendMode; - - TICK++; - - let i; - - for (i = 0; i < this.currentIndex; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - const sprite = sprites[i]; - - nextTexture = sprite._texture.baseTexture; - textureId = nextTexture._id; - - const spriteBlendMode = premultiplyBlendMode[Number(nextTexture.premultiplyAlpha)][sprite.blendMode]; - - if (blendMode !== spriteBlendMode) - { - blendMode = spriteBlendMode; - - // force the batch to break! - currentTexture = null; - textureCount = MAX_TEXTURES; - TICK++; - } - - if (currentTexture !== nextTexture) - { - currentTexture = nextTexture; - - if (nextTexture._enabled !== TICK) - { - if (textureCount === MAX_TEXTURES) - { - TICK++; - - textureCount = 0; - - currentGroup.size = i - currentGroup.start; - - currentGroup = groups[groupCount++]; - currentGroup.textureCount = 0; - currentGroup.blend = blendMode; - currentGroup.start = i; - } - - nextTexture.touched = touch; - nextTexture._enabled = TICK; - nextTexture._id = textureCount; - - currentGroup.textures[currentGroup.textureCount++] = nextTexture; - textureCount++; - } - } - - vertexData = sprite.vertexData; - - // TODO this sum does not need to be set each frame.. - uvs = sprite._texture._uvs.uvsUint32; - textureId = nextTexture._id; - - if (this.renderer.roundPixels) - { - const resolution = this.renderer.resolution; - - // xy - float32View[index] = ((vertexData[0] * resolution) | 0) / resolution; - float32View[index + 1] = ((vertexData[1] * resolution) | 0) / resolution; - - // xy - float32View[index + 5] = ((vertexData[2] * resolution) | 0) / resolution; - float32View[index + 6] = ((vertexData[3] * resolution) | 0) / resolution; - - // xy - float32View[index + 10] = ((vertexData[4] * resolution) | 0) / resolution; - float32View[index + 11] = ((vertexData[5] * resolution) | 0) / resolution; - - // xy - float32View[index + 15] = ((vertexData[6] * resolution) | 0) / resolution; - float32View[index + 16] = ((vertexData[7] * resolution) | 0) / resolution; - } - else - { - // xy - float32View[index] = vertexData[0]; - float32View[index + 1] = vertexData[1]; - - // xy - float32View[index + 5] = vertexData[2]; - float32View[index + 6] = vertexData[3]; - - // xy - float32View[index + 10] = vertexData[4]; - float32View[index + 11] = vertexData[5]; - - // xy - float32View[index + 15] = vertexData[6]; - float32View[index + 16] = vertexData[7]; - } - - uint32View[index + 2] = uvs[0]; - uint32View[index + 7] = uvs[1]; - uint32View[index + 12] = uvs[2]; - uint32View[index + 17] = uvs[3]; - /* eslint-disable max-len */ - const alpha = Math.min(sprite.worldAlpha, 1.0); - const argb = alpha < 1.0 && nextTexture.premultiplyAlpha ? premultiplyTint(sprite._tintRGB, alpha) - : sprite._tintRGB + (alpha * 255 << 24); - - uint32View[index + 3] = uint32View[index + 8] = uint32View[index + 13] = uint32View[index + 18] = argb; - - float32View[index + 4] = float32View[index + 9] = float32View[index + 14] = float32View[index + 19] = textureId; - /* eslint-enable max-len */ - - index += 20; - } - - currentGroup.size = i - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this.vaoMax <= this.vertexCount) - { - this.vaoMax++; - - const buffer = new Buffer(null, false); - - /* eslint-disable max-len */ - this.vaos[this.vertexCount] = new Geometry() - .addAttribute('aVertexPosition', buffer, 2, false, gl.FLOAT) - .addAttribute('aTextureCoord', buffer, 2, true, gl.UNSIGNED_SHORT) - .addAttribute('aColor', buffer, 4, true, gl.UNSIGNED_BYTE) - .addAttribute('aTextureId', buffer, 1, true, gl.FLOAT) - .addIndex(this.indexBuffer); - /* eslint-enable max-len */ - - this.vertexBuffers[this.vertexCount] = buffer; - } - - this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); - this.renderer.geometry.bind(this.vaos[this.vertexCount]); - - this.vertexCount++; - } - else - { - // lets use the faster option, always use buffer number 0 - this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); - - this.renderer.geometry.updateBuffers(); - } - - // / render the groups.. - for (i = 0; i < groupCount; i++) - { - const group = groups[i]; - const groupTextureCount = group.textureCount; - - for (let j = 0; j < groupTextureCount; j++) - { - this.renderer.texture.bind(group.textures[j], j); - } - - // set the blend mode.. - this.renderer.state.setBlendMode(group.blend); - - gl.drawElements(gl.TRIANGLES, group.size * 6, gl.UNSIGNED_SHORT, group.start * 6 * 2); - } - - // reset elements for the next flush - this.currentIndex = 0; - } - - /** - * Starts a new sprite batch. - */ - start() - { - this.renderer.shader.bind(this.shader); - - if (settings.CAN_UPLOAD_SAME_BUFFER) - { - // bind buffer #0, we don't need others - this.renderer.geometry.bind(this.vaos[this.vertexCount]); - } - } - - /** - * Stops and flushes the current batch. - * - */ - stop() - { - this.flush(); - } - - /** - * Destroys the SpriteRenderer. - * - */ - destroy() - { - for (let i = 0; i < this.vaoMax; i++) - { - if (this.vertexBuffers[i]) - { - this.vertexBuffers[i].destroy(); - } - if (this.vaos[i]) - { - this.vaos[i].destroy(); - } - } - - if (this.indexBuffer) - { - this.indexBuffer.destroy(); - } - - this.renderer.off('prerender', this.onPrerender, this); - - if (this.shader) - { - this.shader.destroy(); - this.shader = null; - } - - this.vertexBuffers = null; - this.vaos = null; - this.indexBuffer = null; - this.indices = null; - - this.sprites = null; - - for (let i = 0; i < this.buffers.length; ++i) - { - this.buffers[i].destroy(); - } - - super.destroy(); - } -} diff --git a/packages/sprite/src/generateMultiTextureShader.js b/packages/sprite/src/generateMultiTextureShader.js deleted file mode 100644 index f2e27be..0000000 --- a/packages/sprite/src/generateMultiTextureShader.js +++ /dev/null @@ -1,69 +0,0 @@ -import { Shader, UniformGroup } from '@pixi/core'; -import vertex from './texture.vert'; - -const fragTemplate = [ - 'varying vec2 vTextureCoord;', - 'varying vec4 vColor;', - 'varying float vTextureId;', - 'uniform sampler2D uSamplers[%count%];', - - 'void main(void){', - 'vec4 color;', - 'float textureId = floor(vTextureId+0.5);', - '%forloop%', - 'gl_FragColor = color * vColor;', - '}', -].join('\n'); - -export default function generateMultiTextureShader(gl, maxTextures) -{ - const sampleValues = new Int32Array(maxTextures); - - for (let i = 0; i < maxTextures; i++) - { - sampleValues[i] = i; - } - - const uniforms = { - default: UniformGroup.from({ uSamplers: sampleValues }, true), - }; - - let fragmentSrc = fragTemplate; - - fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); - fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); - - const shader = Shader.from(vertex, fragmentSrc, uniforms); - - return shader; -} - -function generateSampleSrc(maxTextures) -{ - let src = ''; - - src += '\n'; - src += '\n'; - - for (let i = 0; i < maxTextures; i++) - { - if (i > 0) - { - src += '\nelse '; - } - - if (i < maxTextures - 1) - { - src += `if(textureId == ${i}.0)`; - } - - src += '\n{'; - src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; - src += '\n}'; - } - - src += '\n'; - src += '\n'; - - return src; -} diff --git a/packages/sprite/src/index.js b/packages/sprite/src/index.js index edf477c..c5179d7 100644 --- a/packages/sprite/src/index.js +++ b/packages/sprite/src/index.js @@ -1,2 +1 @@ export { default as Sprite } from './Sprite'; -export { default as SpriteRenderer } from './SpriteRenderer'; diff --git a/packages/sprite/src/texture.vert b/packages/sprite/src/texture.vert deleted file mode 100644 index 18b89ff..0000000 --- a/packages/sprite/src/texture.vert +++ /dev/null @@ -1,19 +0,0 @@ -precision highp float; -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; -attribute vec4 aColor; -attribute float aTextureId; - -uniform mat3 projectionMatrix; - -varying vec2 vTextureCoord; -varying vec4 vColor; -varying float vTextureId; - -void main(void){ - gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = aTextureCoord; - vTextureId = aTextureId; - vColor = aColor; -} diff --git a/packages/sprite/test/SpriteRenderer.js b/packages/sprite/test/SpriteRenderer.js deleted file mode 100644 index 35758ad..0000000 --- a/packages/sprite/test/SpriteRenderer.js +++ /dev/null @@ -1,43 +0,0 @@ -const { SpriteRenderer } = require('../'); - -const mockrunner = { - contextChange: { - remove: () => 1, - add: () => 1, - }, -}; - -describe('SpriteRenderer', function () -{ - it('can be destroyed', function () - { - const destroyable = { destroy: sinon.stub() }; - const webgl = { - on: sinon.stub(), - runners: mockrunner, - off: sinon.stub(), - }; - const renderer = new SpriteRenderer(webgl); - - // simulate onContextChange - renderer.vertexBuffers = [destroyable, destroyable]; - renderer.vaos = [destroyable, destroyable]; - renderer.indexBuffer = destroyable; - renderer.shader = destroyable; - - expect(() => renderer.destroy()).to.not.throw(); - }); - - it('can be destroyed immediately', function () - { - const webgl = { - on: sinon.stub(), - runners: mockrunner, - off: sinon.stub(), - }; - - const renderer = new SpriteRenderer(webgl); - - expect(() => renderer.destroy()).to.not.throw(); - }); -}); diff --git a/packages/sprite/test/index.js b/packages/sprite/test/index.js index 76e90bc..b5ea071 100644 --- a/packages/sprite/test/index.js +++ b/packages/sprite/test/index.js @@ -1,2 +1 @@ require('./Sprite'); -require('./SpriteRenderer'); diff --git a/tools/integration-tests/package.json b/tools/integration-tests/package.json index 44e2bed..712d250 100644 --- a/tools/integration-tests/package.json +++ b/tools/integration-tests/package.json @@ -18,8 +18,10 @@ "@pixi/graphics": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/mesh": "^5.0.0-alpha.3", + "@pixi/mesh-extras": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", "@pixi/text": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3", "floss": "^2.1.3" } } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a0cd48e..f7ea0ad 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -218,15 +218,15 @@ Object.defineProperties(PIXI.mesh, { /** * @class PIXI.mesh.Mesh - * @see PIXI.Mesh2d + * @see PIXI.SimpleMesh * @deprecated since 5.0.0 */ Mesh: { get() { - deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.Mesh2d'); + deprecation(v5, 'PIXI.mesh.Mesh has moved to PIXI.SimpleMesh'); - return PIXI.Mesh2d; + return PIXI.SimpleMesh; }, }, /** @@ -244,28 +244,28 @@ }, /** * @class PIXI.mesh.Plane - * @see PIXI.Plane + * @see PIXI.SimplePlane * @deprecated since 5.0.0 */ Plane: { get() { - deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.Plane'); + deprecation(v5, 'PIXI.mesh.Plane has moved to PIXI.SimplePlane'); - return PIXI.Plane; + return PIXI.SimplePlane; }, }, /** * @class PIXI.mesh.Rope - * @see PIXI.Rope + * @see PIXI.SimpleRope * @deprecated since 5.0.0 */ Rope: { get() { - deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.Rope'); + deprecation(v5, 'PIXI.mesh.Rope has moved to PIXI.SimpleRope'); - return PIXI.Rope; + return PIXI.SimpleRope; }, }, /** diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 6fdabf0..a46a3d9 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -35,12 +35,10 @@ // Install renderer plugins core.Renderer.registerPlugin('accessibility', accessibility.AccessibilityManager); core.Renderer.registerPlugin('extract', extract.Extract); -core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); -core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); -core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); +core.Renderer.registerPlugin('batch', core.BatchRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); loaders.Loader.registerPlugin(textBitmap.BitmapFontLoader); diff --git a/package.json b/package.json index 85d9074..dd6db1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "start": "npm run watch", "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "clean:build": "rimraf \"{bundles,packages,packages/canvas,packages/filters}/*/{lib,dist}\"", diff --git a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js index 27e9912..15ee89b 100644 --- a/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js +++ b/packages/canvas/canvas-graphics/src/CanvasGraphicsRenderer.js @@ -55,20 +55,24 @@ if (graphics.canvasTintDirty !== graphics.dirty || graphics._prevTint !== graphics.tint) { - this.updateGraphicsTint(graphics); + // this.updateGraphicsTint(graphics); } renderer.setBlendMode(graphics.blendMode); - for (let i = 0; i < graphics.graphicsData.length; i++) + const graphicsData = graphics.geometry.graphicsData; + + for (let i = 0; i < graphicsData.length; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; - const fillColor = data._fillTint; - const lineColor = data._lineTint; + const fillColor = fillStyle.color;// data._fillTint; + const lineColor = lineStyle.color;// data._lineTint; - context.lineWidth = data.lineWidth; + context.lineWidth = lineStyle.width; if (data.type === SHAPES.POLY) { @@ -78,33 +82,35 @@ for (let j = 0; j < data.holes.length; j++) { - this.renderPolygon(data.holes[j].points, true, context); + // this.renderPolygon(data.holes[j].points, true, context); } - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; + context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } } else if (data.type === SHAPES.RECT) { - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fillRect(shape.x, shape.y, shape.width, shape.height); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.strokeRect(shape.x, shape.y, shape.width, shape.height); } @@ -116,15 +122,16 @@ context.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -157,15 +164,15 @@ context.closePath(); - if (data.fill) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } @@ -194,16 +201,15 @@ context.quadraticCurveTo(rx, ry, rx, ry + radius); context.closePath(); - if (data.fillColor || data.fillColor === 0) + if (fillStyle.visible) { - context.globalAlpha = data.fillAlpha * worldAlpha; + context.globalAlpha = fillStyle.alpha * worldAlpha; context.fillStyle = `#${(`00000${(fillColor | 0).toString(16)}`).substr(-6)}`; context.fill(); } - - if (data.lineWidth) + if (lineStyle.visible) { - context.globalAlpha = data.lineAlpha * worldAlpha; + context.globalAlpha = lineStyle.alpha * worldAlpha; context.strokeStyle = `#${(`00000${(lineColor | 0).toString(16)}`).substr(-6)}`; context.stroke(); } diff --git a/packages/canvas/canvas-graphics/src/Graphics.js b/packages/canvas/canvas-graphics/src/Graphics.js index 2980573..621cb09 100644 --- a/packages/canvas/canvas-graphics/src/Graphics.js +++ b/packages/canvas/canvas-graphics/src/Graphics.js @@ -45,6 +45,8 @@ return texture; }; +Graphics.prototype.cachedGraphicsData = []; + /** * Renders the object using the Canvas renderer * @@ -60,5 +62,6 @@ return; } + this.finishPoly(); renderer.plugins.graphics.render(this); }; diff --git a/packages/canvas/canvas-mesh/src/Mesh2d.js b/packages/canvas/canvas-mesh/src/Mesh2d.js deleted file mode 100644 index d0bd80f..0000000 --- a/packages/canvas/canvas-mesh/src/Mesh2d.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Mesh2d } from '@pixi/mesh-extras'; -import { settings } from './settings'; - -// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created -// this was merely created to completely decouple canvas from the base Mesh class and we are -// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. - -/** - * Internal variable for `canvasPadding`. - * - * @private - * @memberof PIXI.Mesh2d - * @member {number} - * @default null - */ -Mesh2d.prototype._canvasPadding = null; - -/** - * Triangles in canvas mode are automatically antialiased, use this value to force triangles - * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} - * - * @see PIXI.settings.MESH_CANVAS_PADDING - * @member {number} canvasPadding - * @memberof PIXI.Mesh2d# - * @default 0 - */ -Object.defineProperty(Mesh2d.prototype, 'canvasPadding', { - get() - { - return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; - }, - set(value) - { - this._canvasPadding = value; - }, -}); diff --git a/packages/canvas/canvas-mesh/src/SimpleMesh.js b/packages/canvas/canvas-mesh/src/SimpleMesh.js new file mode 100644 index 0000000..1af9ea6 --- /dev/null +++ b/packages/canvas/canvas-mesh/src/SimpleMesh.js @@ -0,0 +1,36 @@ +import { SimpleMesh } from '@pixi/mesh-extras'; +import { settings } from './settings'; + +// IMPORTANT: Please do NOT use this as a precedent to use `settings` after the object is created +// this was merely created to completely decouple canvas from the base Mesh class and we are +// unable to add `canvasPadding` in the constructor anymore, as the case was for PixiJS v4. + +/** + * Internal variable for `canvasPadding`. + * + * @private + * @memberof PIXI.SimpleMesh + * @member {number} + * @default null + */ +SimpleMesh.prototype._canvasPadding = null; + +/** + * Triangles in canvas mode are automatically antialiased, use this value to force triangles + * to overlap a bit with each other. To set the global default, set {@link PIXI.settings.MESH_CANVAS_PADDING} + * + * @see PIXI.settings.MESH_CANVAS_PADDING + * @member {number} canvasPadding + * @memberof PIXI.SimpleMesh# + * @default 0 + */ +Object.defineProperty(SimpleMesh.prototype, 'canvasPadding', { + get() + { + return this._canvasPadding !== null ? this._canvasPadding : settings.MESH_CANVAS_PADDING; + }, + set(value) + { + this._canvasPadding = value; + }, +}); diff --git a/packages/canvas/canvas-mesh/src/index.js b/packages/canvas/canvas-mesh/src/index.js index f4fa31d..b563a68 100644 --- a/packages/canvas/canvas-mesh/src/index.js +++ b/packages/canvas/canvas-mesh/src/index.js @@ -1,6 +1,6 @@ export { default as CanvasMeshRenderer } from './CanvasMeshRenderer'; import './settings'; -import './Mesh2d'; +import './SimpleMesh'; import './NineSlicePlane'; import './Mesh'; diff --git a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js index ce2b061..fe2744d 100644 --- a/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js +++ b/packages/canvas/canvas-renderer/src/utils/CanvasMaskManager.js @@ -59,7 +59,8 @@ renderGraphicsShape(graphics) { const context = this.renderer.context; - const len = graphics.graphicsData.length; + const graphicsData = graphics.geometry.graphicsData; + const len = graphicsData.length; if (len === 0) { @@ -70,7 +71,7 @@ for (let i = 0; i < len; i++) { - const data = graphics.graphicsData[i]; + const data = graphicsData[i]; const shape = data.shape; if (data.type === SHAPES.POLY) diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index cc3618b..9126ed3 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -10,5 +10,5 @@ */ Sprite.prototype._renderCanvas = function _renderCanvas(renderer) { - renderer.plugins[this.pluginName].render(this); + renderer.plugins.sprite.render(this); }; diff --git a/packages/core/src/batch/BatchBuffer.js b/packages/core/src/batch/BatchBuffer.js new file mode 100644 index 0000000..de3ba21 --- /dev/null +++ b/packages/core/src/batch/BatchBuffer.js @@ -0,0 +1,39 @@ +/** + * @class + * @memberof PIXI + */ +export default class BatchBuffer +{ + /** + * @param {number} size - The size of the buffer in bytes. + */ + constructor(size) + { + this.vertices = new ArrayBuffer(size); + + /** + * View on the vertices as a Float32Array for positions + * + * @member {Float32Array} + */ + this.float32View = new Float32Array(this.vertices); + + /** + * View on the vertices as a Uint32Array for uvs + * + * @member {Float32Array} + */ + this.uint32View = new Uint32Array(this.vertices); + } + + /** + * Destroys the buffer. + * + */ + destroy() + { + this.vertices = null; + this.float32View = null; + this.uint32View = null; + } +} diff --git a/packages/core/src/batch/BatchGeometry.js b/packages/core/src/batch/BatchGeometry.js new file mode 100644 index 0000000..2ae4a46 --- /dev/null +++ b/packages/core/src/batch/BatchGeometry.js @@ -0,0 +1,40 @@ +import { TYPES } from '@pixi/constants'; +import Geometry from '../geometry/Geometry'; +import Buffer from '../geometry/Buffer'; + +/** + * Geometry used to batch standard PixiJS content (e.g., Mesh, Sprite, Graphics objects). + * @class + * @memberof PIXI + */ +export default class BatchGeometry extends Geometry +{ + /** + * @param {boolean} [_static=false] Optimization flag, where `false` + * is updated every frame, `true` doesn't change frame-to-frame. + */ + constructor(_static = false) + { + super(); + + /** + * Buffer used for position, color, texture IDs + * @member {PIXI.Buffer} + * @private + */ + this._buffer = new Buffer(null, _static, false); + + /** + * Index buffer data + * @member {PIXI.Buffer} + * @private + */ + this._indexBuffer = new Buffer(null, _static, true); + + this.addAttribute('aVertexPosition', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', this._buffer, 2, false, TYPES.FLOAT) + .addAttribute('aColor', this._buffer, 4, true, TYPES.UNSIGNED_BYTE) + .addAttribute('aTextureId', this._buffer, 1, true, TYPES.FLOAT) + .addIndex(this._indexBuffer); + } +} diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js new file mode 100644 index 0000000..19e5cf0 --- /dev/null +++ b/packages/core/src/batch/BatchRenderer.js @@ -0,0 +1,511 @@ +import BatchGeometry from './BatchGeometry'; +import State from '../state/State'; +import ObjectRenderer from './ObjectRenderer'; +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; + +import BatchBuffer from './BatchBuffer'; +import generateMultiTextureShader from './generateMultiTextureShader'; +import { ENV } from '@pixi/constants'; + +let TICK = 0; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * @class + * @private + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class BatchRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * Number of values sent in the vertex buffer. + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 + * + * @member {number} + */ + this.vertSize = 6; + + /** + * The size of the vertex information in bytes. + * + * @member {number} + */ + this.vertByteSize = this.vertSize * 4; + + /** + * The number of images in the SpriteRenderer before it flushes. + * + * @member {number} + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop + + this.currentSize = 0; + this.currentIndexSize = 0; + + // the total number of bytes in our batch + // let numVerts = this.size * 4 * this.vertByteSize; + + this.attributeBuffers = {}; + this.aBuffers = {}; + this.iBuffers = {}; + + // this.defualtSpriteIndexBuffer = new Buffer(createIndicesForQuads(this.size), true, true); + + /** + * Holds the defualt indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + // const indicies = createIndicesForQuads(this.size); + + // this.defaultQuadIndexBuffer = new Buffer(indicies, true, true); + + this.onlySprites = false; + + /** + * The default shaders that is used if a sprite doesn't have a more specific one. + * there is a shader for each number of textures that can be rendered. + * These shaders will also be generated on the fly as required. + * @member {PIXI.Shader[]} + */ + this.shader = null; + + this.currentIndex = 0; + this.groups = []; + + for (let k = 0; k < this.size / 4; k++) + { + this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; + } + + this.elements = []; + + this.vaos = []; + + this.vaoMax = 2; + this.vertexCount = 0; + + this.renderer.on('prerender', this.onPrerender, this); + this.state = State.for2d(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); + } + + // generate generateMultiTextureProgram, may be a better move? + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser may omit aTextureId + // as it is not used by the shader so is optimized out. + for (let i = 0; i < this.vaoMax; i++) + { + /* eslint-disable max-len */ + this.vaos[i] = new BatchGeometry(); + } + } + + /** + * Called before the renderer starts rendering. + * + */ + onPrerender() + { + this.vertexCount = 0; + } + + /** + * Renders the sprite object. + * + * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this.currentSize + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this.elements[this.currentIndex++] = element; + + this.currentSize += element.vertexData.length / 2; + this.currentIndexSize += element.indices.length; + } + + getIndexBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.iBuffers[roundedSize]; + + if (!buffer) + { + this.iBuffers[roundedSize] = buffer = new Uint16Array(roundedSize); + } + + return buffer; + } + + getAttributeBuffer(size) + { + const roundedSize = Math.ceil(size / 100.0) * 100; + + let buffer = this.aBuffers[roundedSize]; + + if (!buffer) + { + this.aBuffers[roundedSize] = buffer = new BatchBuffer(roundedSize * this.vertByteSize); + } + + return buffer; + } + + /** + * Renders the content and empties the current batch. + * + */ + flush() + { + if (this.currentSize === 0) + { + return; + } + + const gl = this.renderer.gl; + const MAX_TEXTURES = this.MAX_TEXTURES; + + const buffer = this.getAttributeBuffer(this.currentSize); + const indexBuffer = this.getIndexBuffer(this.currentIndexSize); + + const elements = this.elements; + const groups = this.groups; + + const float32View = buffer.float32View; + const uint32View = buffer.uint32View; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let indexCount = 0; + let nextTexture; + let currentTexture; + let groupCount = 0; + + let textureCount = 0; + let currentGroup = groups[0]; + + let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + TICK++; + + let i; + + // console.log("<>") + for (i = 0; i < this.currentIndex; ++i) + { + // upload the sprite elements... + // they have all ready been calculated so we just need to push them into the buffer. + + const sprite = elements[i]; + + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = indexCount - currentGroup.start; + + currentGroup = groups[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = indexCount; + } + + nextTexture.touched = touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.renderGeometry(sprite, float32View, uint32View, indexBuffer, index, indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * 6; + indexCount += sprite.indices.length; + } + + currentGroup.size = indexCount - currentGroup.start; + + // this.indexBuffer.update(); + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { + // this is still needed for IOS performance.. + // it really does not like uploading to the same buffer in a single frame! + if (this.vaoMax <= this.vertexCount) + { + this.vaoMax++; + /* eslint-disable max-len */ + this.vaos[this.vertexCount] = new BatchGeometry(); + } + + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + + this.renderer.geometry.updateBuffers(); + + this.vertexCount++; + } + else + { + // lets use the faster option, always use buffer number 0 + this.vaos[this.vertexCount]._buffer.update(buffer.vertices, 0); + this.vaos[this.vertexCount]._indexBuffer.update(indexBuffer, 0); + + // if (true)// this.spriteOnly) + // { + // this.vaos[this.vertexCount].indexBuffer = this.defualtSpriteIndexBuffer; + // this.vaos[this.vertexCount].buffers[1] = this.defualtSpriteIndexBuffer; + // } + + this.renderer.geometry.updateBuffers(); + } + + // this.renderer.state.set(this.state); + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + // e.log(groupCount); + // / render the groups.. + + for (i = 0; i < groupCount; i++) + { + const group = groups[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + } + + // this.state.blendMode = group.blend; + // this.state.blend = true; + + // this.renderer.state.setState(this.state); + // set the blend mode.. + stateSystem.setBlendMode(group.blend); + + gl.drawElements(gl.TRIANGLES, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this.currentIndex = 0; + this.currentSize = 0; + this.currentIndexSize = 0; + } + + renderGeometry(element, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6;// float32View.length / 6 / 2; + const uvs = element.uvs; + const indicies = element.indices;// geometry.getIndex().data;// indicies; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + + const argb = alpha < 1.0 && element._texture.baseTexture.premultiplyAlpha ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // console.log(element._texture.baseTexture.premultiplyAlpha); + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[index++] = vertexData[i]; + float32View[index++] = vertexData[i + 1]; + float32View[index++] = uvs[i]; + float32View[index++] = uvs[i + 1]; + uint32View[index++] = argb; + float32View[index++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[indexCount++] = p + indicies[i]; + } + } + /* + renderQuad(vertexData, uvs, argb, textureId, float32View, uint32View, indexBuffer, index, indexCount) + { + const p = index / 6; + + float32View[index++] = vertexData[0]; + float32View[index++] = vertexData[1]; + float32View[index++] = uvs.x0; + float32View[index++] = uvs.y0; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[2]; + float32View[index++] = vertexData[3]; + float32View[index++] = uvs.x1; + float32View[index++] = uvs.y1; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[4]; + float32View[index++] = vertexData[5]; + float32View[index++] = uvs.x2; + float32View[index++] = uvs.y2; + uint32View[index++] = argb; + float32View[index++] = textureId; + + float32View[index++] = vertexData[6]; + float32View[index++] = vertexData[7]; + float32View[index++] = uvs.x3; + float32View[index++] = uvs.y3; + uint32View[index++] = argb; + float32View[index++] = textureId; + + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 1; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 0; + indexBuffer[indexCount++] = p + 2; + indexBuffer[indexCount++] = p + 3; + } +*/ + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.shader.bind(this.shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this.vaos[this.vertexCount]); + } + } + + /** + * Stops and flushes the current batch. + * + */ + stop() + { + this.flush(); + } + + /** + * Destroys the SpriteRenderer. + * + */ + destroy() + { + for (let i = 0; i < this.vaoMax; i++) + { + // if (this.vertexBuffers[i]) + // { + // this.vertexBuffers[i].destroy(); + // } + if (this.vaos[i]) + { + this.vaos[i].destroy(); + } + } + + if (this.indexBuffer) + { + this.indexBuffer.destroy(); + } + + this.renderer.off('prerender', this.onPrerender, this); + + if (this.shader) + { + this.shader.destroy(); + this.shader = null; + } + + // this.vertexBuffers = null; + this.vaos = null; + this.indexBuffer = null; + this.indices = null; + this.sprites = null; + + // for (let i = 0; i < this.buffers.length; ++i) + // { + // this.buffers[i].destroy(); + // } + + super.destroy(); + } +} diff --git a/packages/core/src/batch/generateMultiTextureShader.js b/packages/core/src/batch/generateMultiTextureShader.js new file mode 100644 index 0000000..95bac2e --- /dev/null +++ b/packages/core/src/batch/generateMultiTextureShader.js @@ -0,0 +1,85 @@ +import Shader from '../shader/Shader'; +import Program from '../shader/Program'; +import UniformGroup from '../shader/UniformGroup'; + +import vertex from './texture.vert'; +import { Matrix } from '@pixi/math'; + +const fragTemplate = [ + 'varying vec2 vTextureCoord;', + 'varying vec4 vColor;', + 'varying float vTextureId;', + 'uniform sampler2D uSamplers[%count%];', + + 'void main(void){', + 'vec4 color;', + 'float textureId = floor(vTextureId+0.5);', + '%forloop%', + 'gl_FragColor = color * vColor;', + '}', +].join('\n'); + +const defaultGroupCache = {}; +const programCache = {}; + +export default function generateMultiTextureShader(gl, maxTextures) +{ + if (!programCache[maxTextures]) + { + const sampleValues = new Int32Array(maxTextures); + + for (let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + + defaultGroupCache[maxTextures] = UniformGroup.from({ uSamplers: sampleValues }, true); + + let fragmentSrc = fragTemplate; + + fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); + fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); + + programCache[maxTextures] = new Program(vertex, fragmentSrc); + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: defaultGroupCache[maxTextures], + }; + + const shader = new Shader(programCache[maxTextures], uniforms); + + return shader; +} + +function generateSampleSrc(maxTextures) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + for (let i = 0; i < maxTextures; i++) + { + if (i > 0) + { + src += '\nelse '; + } + + if (i < maxTextures - 1) + { + src += `if(textureId == ${i}.0)`; + } + + src += '\n{'; + src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +} diff --git a/packages/core/src/batch/texture.vert b/packages/core/src/batch/texture.vert new file mode 100644 index 0000000..a9ca159 --- /dev/null +++ b/packages/core/src/batch/texture.vert @@ -0,0 +1,21 @@ +precision highp float; +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; +attribute float aTextureId; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform vec3 tint; + +varying vec2 vTextureCoord; +varying vec4 vColor; +varying float vTextureId; + +void main(void){ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vTextureId = aTextureId; + vColor = aColor * vec4(tint,1.); +} diff --git a/packages/core/src/context/ContextSystem.js b/packages/core/src/context/ContextSystem.js index 2542fa6..202d6eb 100644 --- a/packages/core/src/context/ContextSystem.js +++ b/packages/core/src/context/ContextSystem.js @@ -27,7 +27,7 @@ /** * Extensions being used - * @name {object} + * @member {object} * @readonly * @property {WEBGL_draw_buffers} drawBuffers - WebGL v1 extension * @property {WEBKIT_WEBGL_depth_texture} depthTexture - WebGL v1 extension diff --git a/packages/core/src/filters/FilterSystem.js b/packages/core/src/filters/FilterSystem.js index 81dc329..fe37118 100644 --- a/packages/core/src/filters/FilterSystem.js +++ b/packages/core/src/filters/FilterSystem.js @@ -98,6 +98,13 @@ super(renderer); /** + * List of filters for the FilterSystem + * @member {Array} + * @readonly + */ + this.defaultFilterStack = [{}]; + + /** * stores a bunch of PO2 textures used for filtering * @member {Object} */ @@ -169,7 +176,7 @@ push(target, filters) { const renderer = this.renderer; - const filterStack = this.renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = this.statePool.pop() || new FilterState(); let resolution = filters[0].resolution; @@ -191,6 +198,11 @@ legacy = legacy || filter.legacy; } + if (filterStack.length === 1) + { + this.defaultFilterStack[0].renderTexture = renderer.renderTexture.renderTexture; + } + filterStack.push(state); state.resolution = resolution; @@ -217,6 +229,7 @@ state.destinationFrame.height = state.renderTexture.height; state.renderTexture.filterFrame = state.sourceFrame; + renderer.renderTexture.bind(state.renderTexture, state.sourceFrame);// /, state.destinationFrame); renderer.renderTexture.clear(); } @@ -227,8 +240,7 @@ */ pop() { - const renderer = this.renderer; - const filterStack = renderer.renderTexture.defaultFilterStack; + const filterStack = this.defaultFilterStack; const state = filterStack.pop(); const filters = state.filters; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 7c47f11..b795638 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -19,6 +19,9 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as BatchRenderer } from './batch/BatchRenderer'; +export { default as BatchGeometry } from './batch/BatchGeometry'; +export { default as generateMultiTextureShader } from './batch/generateMultiTextureShader'; export { default as Quad } from './utils/Quad'; export { default as QuadUv } from './utils/QuadUv'; export { default as checkMaxIfStatementsInShader } from './shader/utils/checkMaxIfStatementsInShader'; diff --git a/packages/core/src/mask/MaskSystem.js b/packages/core/src/mask/MaskSystem.js index 04607c1..aef176a 100644 --- a/packages/core/src/mask/MaskSystem.js +++ b/packages/core/src/mask/MaskSystem.js @@ -72,7 +72,7 @@ // be used on render textures more info here: // https://github.com/pixijs/pixi.js/pull/3545 - if (maskData.vertexData) + if (maskData.isSprite) { this.pushSpriteMask(target, maskData); } @@ -112,7 +112,7 @@ */ pop(target, maskData) { - if (maskData.vertexData) + if (maskData.isSprite) { this.popSpriteMask(target, maskData); } diff --git a/packages/core/src/projection/ProjectionSystem.js b/packages/core/src/projection/ProjectionSystem.js index b953079..3133e8d 100644 --- a/packages/core/src/projection/ProjectionSystem.js +++ b/packages/core/src/projection/ProjectionSystem.js @@ -43,6 +43,13 @@ * @readonly */ this.projectionMatrix = new Matrix(); + + /** + * A transform that will be appended to the projection matrix + * if null, nothing will be applied + * @member {PIXI.Matrix} + */ + this.transform = null; } /** @@ -60,8 +67,22 @@ this.calculateProjection(this.destinationFrame, this.sourceFrame, resolution, root); - this.renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; - this.renderer.globalUniforms.update(); + if (this.transform) + { + this.projectionMatrix.append(this.transform); + } + + const renderer = this.renderer; + + renderer.globalUniforms.uniforms.projectionMatrix = this.projectionMatrix; + renderer.globalUniforms.update(); + + // this will work for now + // but would be sweet to stick and even on the global uniforms.. + if (renderer.shader.shader) + { + renderer.shader.syncUniformGroup(renderer.shader.shader.uniforms.globals); + } } /** diff --git a/packages/core/src/renderTexture/RenderTextureSystem.js b/packages/core/src/renderTexture/RenderTextureSystem.js index e3db1a6..413612a 100644 --- a/packages/core/src/renderTexture/RenderTextureSystem.js +++ b/packages/core/src/renderTexture/RenderTextureSystem.js @@ -32,13 +32,6 @@ */ this.defaultMaskStack = []; - /** - * List of filters for the FilterSystem - * @member {Array} - * @readonly - */ - this.defaultFilterStack = [{}]; - // empty render texture? /** * Render texture diff --git a/packages/core/src/shader/Program.js b/packages/core/src/shader/Program.js index b9d55be..13517c7 100644 --- a/packages/core/src/shader/Program.js +++ b/packages/core/src/shader/Program.js @@ -23,8 +23,10 @@ * @param {string} [vertexSrc] - The source of the vertex shader. * @param {string} [fragmentSrc] - The source of the fragment shader. */ - constructor(vertexSrc, fragmentSrc) + constructor(vertexSrc, fragmentSrc, name = 'pixi-shader') { + this.id = UID++; + /** * The vertex shader. * @@ -42,6 +44,9 @@ this.vertexSrc = setPrecision(this.vertexSrc, settings.PRECISION_VERTEX); this.fragmentSrc = setPrecision(this.fragmentSrc, settings.PRECISION_FRAGMENT); + this.vertexSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.vertexSrc}`; + this.fragmentSrc = `#define SHADER_NAME ${name}-${this.id}\n${this.fragmentSrc}`; + // currently this does not extract structs only default types this.extractData(this.vertexSrc, this.fragmentSrc); @@ -49,8 +54,6 @@ this.glPrograms = {}; this.syncUniforms = null; - - this.id = UID++; } /** @@ -197,7 +200,7 @@ * * @returns {PIXI.Shader} an shiny new pixi shader! */ - static from(vertexSrc, fragmentSrc) + static from(vertexSrc, fragmentSrc, name) { const key = vertexSrc + fragmentSrc; @@ -205,7 +208,7 @@ if (!program) { - ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc); + ProgramCache[key] = program = new Program(vertexSrc, fragmentSrc, name); } return program; diff --git a/packages/core/src/state/State.js b/packages/core/src/state/State.js index 0308a28..c97faa7 100644 --- a/packages/core/src/state/State.js +++ b/packages/core/src/state/State.js @@ -159,5 +159,15 @@ this.offsets = !!value; this._polygonOffset = value; } + + static for2d() + { + const state = new State(); + + state.depthTest = false; + state.blend = true; + + return state; + } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index f9bb8bc..e4fef84 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -7,6 +7,8 @@ import { Rectangle, Point } from '@pixi/math'; import { uid, TextureCache, getResolutionOfUrl } from '@pixi/utils'; +const DEFAULT_UVS = new TextureUvs(); + /** * A texture stores the information that represents an image or part of an image. It cannot be added * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided @@ -108,7 +110,7 @@ * @member {PIXI.TextureUvs} * @private */ - this._uvs = null; + this._uvs = DEFAULT_UVS; /** * Default TextureMatrix instance for this texture @@ -146,6 +148,7 @@ // if there is no frame we should monitor for any base texture changes.. baseTexture.on('update', this.onBaseTextureUpdated, this); } + this.frame = frame; } else @@ -268,7 +271,7 @@ */ updateUvs() { - if (!this._uvs) + if (this._uvs === DEFAULT_UVS) { this._uvs = new TextureUvs(); } diff --git a/packages/core/src/textures/TextureSystem.js b/packages/core/src/textures/TextureSystem.js index 55c977c..7781308 100644 --- a/packages/core/src/textures/TextureSystem.js +++ b/packages/core/src/textures/TextureSystem.js @@ -95,12 +95,6 @@ { const { gl } = this; - if (this.currentLocation !== location) - { - this.currentLocation = location; - gl.activeTexture(gl.TEXTURE0 + location); - } - if (texture) { texture = texture.baseTexture || texture; @@ -111,8 +105,13 @@ const glTexture = texture._glTextures[this.CONTEXT_UID] || this.initTexture(texture); + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + if (this.boundTextures[location] !== texture) { + // if (this.currentLocation !== location) + gl.bindTexture(texture.target, glTexture.texture); } @@ -126,6 +125,12 @@ } else { + if (this.currentLocation !== location) + { + this.currentLocation = location; + gl.activeTexture(gl.TEXTURE0 + location); + } + gl.bindTexture(gl.TEXTURE_2D, this.emptyTextures[gl.TEXTURE_2D].texture); this.boundTextures[location] = null; } diff --git a/packages/core/src/textures/TextureUvs.js b/packages/core/src/textures/TextureUvs.js index b52d4bf..0eeba84 100644 --- a/packages/core/src/textures/TextureUvs.js +++ b/packages/core/src/textures/TextureUvs.js @@ -26,7 +26,7 @@ this.x3 = 0; this.y3 = 1; - this.uvsUint32 = new Uint32Array(4); + this.uvsFloat32 = new Float32Array(8); } /** @@ -83,9 +83,13 @@ this.y3 = (frame.y + frame.height) / th; } - this.uvsUint32[0] = (((this.y0 * 65535) & 0xFFFF) << 16) | ((this.x0 * 65535) & 0xFFFF); - this.uvsUint32[1] = (((this.y1 * 65535) & 0xFFFF) << 16) | ((this.x1 * 65535) & 0xFFFF); - this.uvsUint32[2] = (((this.y2 * 65535) & 0xFFFF) << 16) | ((this.x2 * 65535) & 0xFFFF); - this.uvsUint32[3] = (((this.y3 * 65535) & 0xFFFF) << 16) | ((this.x3 * 65535) & 0xFFFF); + this.uvsFloat32[0] = this.x0; + this.uvsFloat32[1] = this.y0; + this.uvsFloat32[2] = this.x1; + this.uvsFloat32[3] = this.y1; + this.uvsFloat32[4] = this.x2; + this.uvsFloat32[5] = this.y2; + this.uvsFloat32[6] = this.x3; + this.uvsFloat32[7] = this.y3; } } diff --git a/packages/core/test/BatchRenderer.js b/packages/core/test/BatchRenderer.js new file mode 100644 index 0000000..3646328 --- /dev/null +++ b/packages/core/test/BatchRenderer.js @@ -0,0 +1,43 @@ +const { BatchRenderer } = require('../'); + +const mockrunner = { + contextChange: { + remove: () => 1, + add: () => 1, + }, +}; + +describe('BatchRenderer', function () +{ + it('can be destroyed', function () + { + const destroyable = { destroy: sinon.stub() }; + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + const renderer = new BatchRenderer(webgl); + + // simulate onContextChange + renderer.vertexBuffers = [destroyable, destroyable]; + renderer.vaos = [destroyable, destroyable]; + renderer.indexBuffer = destroyable; + renderer.shader = destroyable; + + expect(() => renderer.destroy()).to.not.throw(); + }); + + it('can be destroyed immediately', function () + { + const webgl = { + on: sinon.stub(), + runners: mockrunner, + off: sinon.stub(), + }; + + const renderer = new BatchRenderer(webgl); + + expect(() => renderer.destroy()).to.not.throw(); + }); +}); diff --git a/packages/filters/filter-blur/src/generateBlurVertSource.js b/packages/filters/filter-blur/src/generateBlurVertSource.js index a0060ed..09da360 100644 --- a/packages/filters/filter-blur/src/generateBlurVertSource.js +++ b/packages/filters/filter-blur/src/generateBlurVertSource.js @@ -43,11 +43,11 @@ if (x) { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(%sampleIndex% * strength, 0.0), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; } else { - template = 'vBlurTexCoords[%index%] = min( textureCoord + vec2(0.0, %sampleIndex% * strength), inputClamp.zw);'; + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; } for (let i = 0; i < kernelSize; i++) diff --git a/packages/graphics/package.json b/packages/graphics/package.json index 34cd46b..0941261 100644 --- a/packages/graphics/package.json +++ b/packages/graphics/package.json @@ -30,7 +30,8 @@ "@pixi/display": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", - "@pixi/utils": "^5.0.0-alpha.3" + "@pixi/utils": "^5.0.0-alpha.3", + "@pixi/mesh": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index 03dd70d..621d5b7 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -1,191 +1,101 @@ -import { Container, Bounds } from '@pixi/display'; -import { BLEND_MODES } from '@pixi/constants'; -import { Texture } from '@pixi/core'; -import { Point, Rectangle, RoundedRectangle, Ellipse, Polygon, Circle, SHAPES, PI_2 } from '@pixi/math'; -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import bezierCurveTo from './utils/bezierCurveTo'; -import { Sprite } from '@pixi/sprite'; -import GraphicsData from './GraphicsData'; +import { + Circle, + Ellipse, + PI_2, + Point, + Polygon, + Rectangle, + RoundedRectangle, + Matrix, +} from '@pixi/math'; +import { hex2rgb } from '@pixi/utils'; +import { Mesh } from '@pixi/mesh'; +import { Texture, + Shader, + UniformGroup, + generateMultiTextureShader } from '@pixi/core'; +import FillStyle from './styles/FillStyle'; +import GraphicsGeometry from './GraphicsGeometry'; +import LineStyle from './styles/LineStyle'; +import BezierUtils from './utils/BezierUtils'; +import QuadraticUtils from './utils/QuadraticUtils'; +import ArcUtils from './utils/ArcUtils'; +import Star from './utils/Star'; -const tempPoint = new Point(); -const tempColor1 = new Float32Array(4); -const tempColor2 = new Float32Array(4); +const temp = new Float32Array(3); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class - * @extends PIXI.Container + * @extends PIXI.Mesh * @memberof PIXI */ -export default class Graphics extends Container +export default class Graphics extends Mesh { /** - * - * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @param {PIXI.GraphicsGeometry} [geometry=null] - Geometry to use, if omitted + * will create a new GraphicsGeometry instance. */ - constructor(nativeLines = false) + constructor(geometry = null) { - super(); + const ownsGeometry = geometry === null; + + geometry = geometry || new GraphicsGeometry(); + + super(geometry, null, null, 4); // DRAW_MODES.TRIANGLE_STRIP /** - * 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; - - /** - * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * If this Graphics object owns the GraphicsGeometry * * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * The color of any lines drawn. - * - * @member {string} - * @default 0 - */ - this.lineColor = 0; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - * @default 0 - */ - this.lineAlignment = 0.5; - - /** - * Graphics data - * - * @member {PIXI.GraphicsData[]} * @private */ - this.graphicsData = []; + this._ownsGeometry = ownsGeometry; /** - * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to - * reset the tint. + * Current fill style * - * @member {number} - * @default 0xFFFFFF - */ - this.tint = 0xFFFFFF; - - /** - * The previous tint applied to the graphic shape. Used to compare to the current tint and - * check if theres change. - * - * @member {number} + * @member {PIXI.FillStyle} * @private - * @default 0xFFFFFF */ - this._prevTint = 0xFFFFFF; + this._fillStyle = new FillStyle(); /** - * The blend mode to be applied to the graphic shape. Apply a value of - * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. + * Current line style * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL; - * @see PIXI.BLEND_MODES + * @member {PIXI.LineStyle} + * @private */ - this.blendMode = BLEND_MODES.NORMAL; + this._lineStyle = new LineStyle(); + + /** + * Current shape transform matrix. + * + * @member {PIXI.Matrix} + * @private + */ + this._matrix = null; + + /** + * Current hole mode is enabled. + * + * @member {boolean} + * @default false + * @private + */ + this._holeMode = false; /** * Current path * - * @member {PIXI.GraphicsData} + * @member {PIXI.Polygon} * @private */ this.currentPath = null; /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object} - * @private - */ - // TODO - _webgl should use a prototype object, not a random undocumented object... - this._webGL = {}; - - /** - * Whether this shape is being used as a mask. - * - * @member {boolean} - */ - this.isMask = false; - - /** - * The bounds' padding used for bounds calculation. - * - * @member {number} - */ - this.boundsPadding = 0; - - /** - * A cache of the local bounds to prevent recalculation. - * - * @member {PIXI.Rectangle} - * @private - */ - this._localBounds = new Bounds(); - - /** - * 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 = 0; - - /** - * Used to detect if we need to do a fast rect check using the id compare method - * @type {Number} - */ - this.fastRectDirty = -1; - - /** - * Used to detect if we clear the graphics webGL data - * @type {Number} - */ - this.clearDirty = 0; - - /** - * Used to detect if we we need to recalculate local bounds - * @type {Number} - */ - this.boundsDirty = -1; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; - - this._spriteRect = null; - this._fastRect = false; - - this._prevRectTint = null; - this._prevRectFillColor = null; - - /** * 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 @@ -197,6 +107,34 @@ * @memberof PIXI.Graphics# * @default false */ + + /** + * A collections of batches! These can be drawn by the renderer batch system. + * + * @private + * @member {object[]} + */ + this.batches = []; + + /** + * Update dirty for limiting calculating tints for batches. + * + * @private + * @member {number} + * @default -1 + */ + this.batchTint = -1; + + /** + * Copy of the object vertex data. + * + * @private + * @member {Float32Array} + */ + this.vertexData = null; + + // Set default + this.tint = 0xFFFFFF; } /** @@ -207,169 +145,122 @@ */ clone() { - const clone = new Graphics(); + this.finishPoly(); - clone.renderable = this.renderable; - clone.fillAlpha = this.fillAlpha; - clone.lineWidth = this.lineWidth; - clone.lineColor = this.lineColor; - clone.lineAlignment = this.lineAlignment; - clone.tint = this.tint; - clone.blendMode = this.blendMode; - clone.isMask = this.isMask; - clone.boundsPadding = this.boundsPadding; - clone.dirty = 0; - clone.cachedSpriteDirty = this.cachedSpriteDirty; - - // copy graphics data - for (let i = 0; i < this.graphicsData.length; ++i) - { - clone.graphicsData.push(this.graphicsData[i].clone()); - } - - clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; - - clone.updateLocalBounds(); - - return clone; + return new Graphics(this.geometry); } /** - * Calculate length of quadratic curve - * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} - * for the detailed explanation of math behind this. + * The tint applied to the Rope. This is a hex value. A value of + * 0xFFFFFF will remove any tint effect. * - * @private - * @param {number} fromX - x-coordinate of curve start point - * @param {number} fromY - y-coordinate of curve start point - * @param {number} cpX - x-coordinate of curve control point - * @param {number} cpY - y-coordinate of curve control point - * @param {number} toX - x-coordinate of curve end point - * @param {number} toY - y-coordinate of curve end point - * @return {number} Length of quadratic curve + * @member {number} + * @default 0xFFFFFF */ - _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + get tint() { - const ax = fromX - ((2.0 * cpX) + toX); - const ay = fromY - ((2.0 * cpY) + toY); - const bx = 2.0 * ((cpX - 2.0) * fromX); - const by = 2.0 * ((cpY - 2.0) * fromY); - const a = 4.0 * ((ax * ax) + (ay * ay)); - const b = 4.0 * ((ax * bx) + (ay * by)); - const c = (bx * bx) + (by * by); - - const s = 2.0 * Math.sqrt(a + b + c); - const a2 = Math.sqrt(a); - const a32 = 2.0 * a * a2; - const c2 = 2.0 * Math.sqrt(c); - const ba = b / a2; - - return ( - (a32 * s) - + (a2 * b * (s - c2)) - + ( - ((4.0 * c * a) - (b * b)) - * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) - ) - ) / (4.0 * a32); + return this._tint; + } + set tint(value) + { + this._tint = value; } /** - * Calculate length of bezier curve. - * Analytical solution is impossible, since it involves an integral that does not integrate in general. - * Therefore numerical solution is used. + * The current fill style. * - * @private - * @param {number} fromX - Starting point x - * @param {number} fromY - Starting point y - * @param {number} cpX - Control point x - * @param {number} cpY - Control point y - * @param {number} cpX2 - Second Control point x - * @param {number} cpY2 - Second Control point y - * @param {number} toX - Destination point x - * @param {number} toY - Destination point y - * @return {number} Length of bezier curve + * @member {PIXI.FillStyle} + * @readonly */ - _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + get fill() { - const n = 10; - let result = 0.0; - let t = 0.0; - let t2 = 0.0; - let t3 = 0.0; - let nt = 0.0; - let nt2 = 0.0; - let nt3 = 0.0; - let x = 0.0; - let y = 0.0; - let dx = 0.0; - let dy = 0.0; - let prevX = fromX; - let prevY = fromY; - - for (let i = 1; i <= n; ++i) - { - t = i / n; - t2 = t * t; - t3 = t2 * t; - nt = (1.0 - t); - nt2 = nt * nt; - nt3 = nt2 * nt; - - x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); - y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); - dx = prevX - x; - dy = prevY - y; - prevX = x; - prevY = y; - - result += Math.sqrt((dx * dx) + (dy * dy)); - } - - return result; + return this._fillStyle; } /** - * Calculate number of segments for the curve based on its length to ensure its smoothness. + * The current line style. * - * @private - * @param {number} length - length of curve - * @return {number} Number of segments + * @member {PIXI.LineStyle} + * @readonly */ - _segmentsCount(length) + get line() { - let result = Math.ceil(length / Graphics.CURVES.maxLength); - - if (result < Graphics.CURVES.minSegments) - { - result = Graphics.CURVES.minSegments; - } - else if (result > Graphics.CURVES.maxSegments) - { - result = Graphics.CURVES.maxSegments; - } - - return result; + return this._lineStyle; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * - * @param {number} [lineWidth=0] - width of the line to draw, will update the objects stored style + * @param {number} [width=0] - width of the line to draw, will update the objects stored style * @param {number} [color=0] - color of the line to draw, will update the objects stored style * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style - * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {number} [alignment=1] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls */ - lineStyle(lineWidth = 0, color = 0, alpha = 1, alignment = 0.5) + lineStyle(width = 0, color = 0, alpha = 1, alignment = 0.5, native = false) { - this.lineWidth = lineWidth; - this.lineColor = color; - this.lineAlpha = alpha; - this.lineAlignment = alignment; + this.lineTextureStyle(width, Texture.WHITE, color, alpha, null, alignment, native); - if (this.currentPath) + return this; + } + + /** + * Like line style but support texture for line fill. + * + * @param {number} [width=0] - width of the line to draw, will update the objects stored style + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to use + * @param {number} [color=0] - color of the line to draw, will update the objects stored style + * @param {number} [alpha=1] - alpha of the line to draw, will update the objects stored style + * @param {PIXI.Matrix} [textureMatrix=null] Texture matrix to transform texture + * @param {number} [alignment=0.5] - alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) + * @param {boolean} [native=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + lineTextureStyle(width = 0, texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, + matrix = null, alignment = 0.5, native = false) + { + const visible = width > 0 && alpha > 0; + + if (!visible) + { + this._lineStyle.reset(); + } + else + { + if (this.currentPath) + { + if (this.currentPath.points.length) + { + // TODO we need to add a fix here for multiple lines + // with different styles.. + + // const shape = new Polygon(this.currentPath.points.slice(-2)); + + // shape.closed = false; + + // this.drawShape(shape); + } + else + { + // this.currentPath.points.length = 0; + } + } + + Object.assign(this._lineStyle, { + color, + width, + alpha, + matrix, + texture, + alignment, + native, + visible, + }); + } + + /* if (this.currentPath) { if (this.currentPath.shape.points.length) { @@ -383,17 +274,44 @@ 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; - this.currentPath.lineAlignment = this.lineAlignment; + this.currentPath.lineStyle = style; } - } + }*/ return this; } /** + * Start a polygon object internally + * @private + */ + startPoly() + { + this.currentPath = new Polygon(); + this.currentPath.closed = false; + } + + /** + * Finish the polygon object. + * @private + */ + finishPoly() + { + if (this.currentPath) + { + if (this.currentPath.points.length > 2) + { + this.drawShape(this.currentPath); + this.currentPath = null; + } + else + { + this.currentPath.points.length = 0; + } + } + } + + /** * Moves the current drawing position to x, y. * * @param {number} x - the X coordinate to move to @@ -402,10 +320,8 @@ */ moveTo(x, y) { - const shape = new Polygon([x, y]); - - shape.closed = false; - this.drawShape(shape); + this.startPoly(); + this.currentPath.points.push(x, y); return this; } @@ -420,21 +336,47 @@ */ lineTo(x, y) { - const points = this.currentPath.shape.points; + if (!this.currentPath) + { + this.moveTo(0, 0); + } + // remove duplicates.. + const points = this.currentPath.points; const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; if (fromX !== x || fromY !== y) { points.push(x, y); - this.dirty++; } return this; } /** + * Initialize the curve + * + * @private + * @param {number} [x=0] + * @param {number} [y=0] + */ + _initCurve(x = 0, y = 0) + { + if (this.currentPath) + { + if (this.currentPath.points.length === 0) + { + this.currentPath.points = [x, y]; + } + } + else + { + this.moveTo(x, y); + } + } + + /** * 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 * @@ -446,43 +388,16 @@ */ quadraticCurveTo(cpX, cpY, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - let xa = 0; - let ya = 0; + const points = this.currentPath.points; if (points.length === 0) { this.moveTo(0, 0); } - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) - : 20; - - for (let i = 1; i <= n; ++i) - { - const 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)); - } + QuadraticUtils.curveTo(cpX, cpY, toX, toY, points); this.dirty++; @@ -502,30 +417,9 @@ */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points = [0, 0]; - } - } - else - { - this.moveTo(0, 0); - } + this._initCurve(); - const points = this.currentPath.shape.points; - - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - - points.length -= 2; - - const n = Graphics.CURVES.adaptive - ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) - : 20; - - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); + BezierUtils.curveTo(cpX, cpY, cpX2, cpY2, toX, toY, this.currentPath.points); this.dirty++; @@ -546,53 +440,17 @@ */ arcTo(x1, y1, x2, y2, radius) { - if (this.currentPath) - { - if (this.currentPath.shape.points.length === 0) - { - this.currentPath.shape.points.push(x1, y1); - } - } - else - { - this.moveTo(x1, y1); - } + this._initCurve(x1, y1); - const points = this.currentPath.shape.points; - const fromX = points[points.length - 2]; - const fromY = points[points.length - 1]; - const a1 = fromY - y1; - const b1 = fromX - x1; - const a2 = y2 - y1; - const b2 = x2 - x1; - const mm = Math.abs((a1 * b2) - (b1 * a2)); + const points = this.currentPath.points; - if (mm < 1.0e-8 || radius === 0) - { - if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) - { - points.push(x1, y1); - } - } - else - { - const dd = (a1 * a1) + (b1 * b1); - const cc = (a2 * a2) + (b2 * b2); - const tt = (a1 * a2) + (b1 * b2); - const k1 = radius * Math.sqrt(dd) / mm; - const k2 = radius * Math.sqrt(cc) / mm; - const j1 = k1 * tt / dd; - const j2 = k2 * tt / cc; - const cx = (k1 * b2) + (k2 * b1); - const cy = (k1 * a2) + (k2 * a1); - const px = b1 * (k2 + j1); - const py = a1 * (k2 + j1); - const qx = b2 * (k1 + j2); - const qy = a2 * (k1 + j2); - const startAngle = Math.atan2(py - cy, px - cx); - const endAngle = Math.atan2(qy - cy, qx - cx); + const result = ArcUtils.curveTo(x1, y1, x2, y2, radius, points); - this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); + if (result) + { + const { cx, cy, radius, startAngle, endAngle, anticlockwise } = result; + + this.arc(cx, cy, radius, startAngle, endAngle, anticlockwise); } this.dirty++; @@ -631,9 +489,6 @@ } const sweep = endAngle - startAngle; - const segs = Graphics.CURVES.adaptive - ? this._segmentsCount(Math.abs(sweep) * radius) - : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -644,20 +499,11 @@ const startY = cy + (Math.sin(startAngle) * radius); // If the currentPath exists, take its points. Otherwise call `moveTo` to start a path. - let points = this.currentPath ? this.currentPath.shape.points : null; + let points = this.currentPath ? this.currentPath.points : null; if (points) { - // We check how far our start is from the last existing point - const xDiff = Math.abs(points[points.length - 2] - startX); - const yDiff = Math.abs(points[points.length - 1] - startY); - - if (xDiff < 0.001 && yDiff < 0.001) - { - // If the point is very close, we don't add it, since this would lead to artifacts - // during tesselation due to floating point imprecision. - } - else + if (points[points.length - 2] !== startX || points[points.length - 1] !== startY) { points.push(startX, startY); } @@ -665,33 +511,10 @@ else { this.moveTo(startX, startY); - points = this.currentPath.shape.points; + points = this.currentPath.points; } - const theta = sweep / (segs * 2); - const theta2 = theta * 2; - - const cTheta = Math.cos(theta); - const sTheta = Math.sin(theta); - - const segMinus = segs - 1; - - const remainder = (segMinus % 1) / segMinus; - - for (let i = 0; i <= segMinus; ++i) - { - const real = i + (remainder * i); - - const angle = ((theta) + startAngle + (theta2 * real)); - - const c = Math.cos(angle); - const s = -Math.sin(angle); - - points.push( - (((cTheta * c) + (sTheta * s)) * radius) + cx, - (((cTheta * -s) + (sTheta * c)) * radius) + cy - ); - } + ArcUtils.arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points); this.dirty++; @@ -708,18 +531,40 @@ */ beginFill(color = 0, alpha = 1) { - this.filling = true; - this.fillColor = color; - this.fillAlpha = alpha; + return this.beginTextureFill(Texture.WHITE, color, alpha); + } - if (this.currentPath) + /** + * Begin the texture fill + * + * @param {PIXI.Texture} [texture=PIXI.Texture.WHITE] - Texture to fill + * @param {number} [color=0xffffff] - Background to fill behind texture + * @param {number} [alpha=1] - Alpha of fill + * @param {PIXI.Matrix} [textureMatrix=null] - Transform matrix + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + beginTextureFill(texture = Texture.WHITE, color = 0xFFFFFF, alpha = 1, matrix = null) + { + const visible = alpha > 0; + + if (!visible) { - if (this.currentPath.shape.points.length <= 2) + this._fillStyle.reset(); + } + else + { + if (this.currentPath) { - this.currentPath.fill = this.filling; - this.currentPath.fillColor = this.fillColor; - this.currentPath.fillAlpha = this.fillAlpha; + this.finishPoly(); } + + Object.assign(this._fillStyle, { + color, + alpha, + texture, + matrix, + visible, + }); } return this; @@ -732,14 +577,15 @@ */ endFill() { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; + this.finishPoly(); + + this._fillStyle.reset(); return this; } /** + * Draws a rectangle shape. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -749,12 +595,11 @@ */ drawRect(x, y, width, height) { - this.drawShape(new Rectangle(x, y, width, height)); - - return this; + return this.drawShape(new Rectangle(x, y, width, height)); } /** + * Draw a rectangle shape with rounded/beveled corners. * * @param {number} x - The X coord of the top-left of the rectangle * @param {number} y - The Y coord of the top-left of the rectangle @@ -765,9 +610,7 @@ */ drawRoundedRect(x, y, width, height, radius) { - this.drawShape(new RoundedRectangle(x, y, width, height, radius)); - - return this; + return this.drawShape(new RoundedRectangle(x, y, width, height, radius)); } /** @@ -780,9 +623,7 @@ */ drawCircle(x, y, radius) { - this.drawShape(new Circle(x, y, radius)); - - return this; + return this.drawShape(new Circle(x, y, radius)); } /** @@ -796,9 +637,7 @@ */ drawEllipse(x, y, width, height) { - this.drawShape(new Ellipse(x, y, width, height)); - - return this; + return this.drawShape(new Ellipse(x, y, width, height)); } /** @@ -813,9 +652,10 @@ // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments let points = path; - let closed = true; + let closed = true;// !!this._fillStyle; - if (points instanceof Polygon) + // check if data has points.. + if (points.points) { closed = points.closed; points = points.points; @@ -843,6 +683,32 @@ } /** + * Draw any shape. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - Shape to draw + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ + drawShape(shape) + { + if (!this._holeMode) + { + this.geometry.drawShape( + shape, + this._fillStyle.toJSON(), + this._lineStyle.toJSON(), + this._matrix + ); + } + else + { + // console.log('HOLE'); + this.geometry.drawHole(shape, this._matrix); + } + + return this; + } + + /** * Draw a star shape with an arbitrary number of points. * * @param {number} x - Center X position of the star @@ -855,25 +721,7 @@ */ drawStar(x, y, points, radius, innerRadius, rotation = 0) { - innerRadius = innerRadius || radius / 2; - - const startAngle = (-1 * Math.PI / 2) + rotation; - const len = points * 2; - const delta = PI_2 / len; - const polygon = []; - - for (let i = 0; i < len; i++) - { - const r = i % 2 ? innerRadius : radius; - const angle = (i * delta) + startAngle; - - polygon.push( - x + (r * Math.cos(angle)), - y + (r * Math.sin(angle)) - ); - } - - return this.drawPolygon(polygon); + return this.drawPolygon(new Star(x, y, points, radius, innerRadius, rotation)); } /** @@ -883,20 +731,10 @@ */ clear() { - if (this.lineWidth || this.filling || this.graphicsData.length > 0) - { - this.lineWidth = 0; - this.lineAlignment = 0.5; + this.geometry.clear(); - this.filling = false; - - this.boundsDirty = -1; - this.canvasTintDirty = -1; - this.dirty++; - this.clearDirty++; - this.graphicsData.length = 0; - } - + this._matrix = null; + this._holeMode = false; this.currentPath = null; this._spriteRect = null; @@ -911,9 +749,11 @@ */ isFastRect() { - return this.graphicsData.length === 1 - && this.graphicsData[0].shape.type === SHAPES.RECT - && !this.graphicsData[0].lineWidth; + // will fix this! + return false; + // this.graphicsData.length === 1 + // && this.graphicsData[0].shape.type === SHAPES.RECT + // && !this.graphicsData[0].lineWidth; } /** @@ -924,96 +764,148 @@ */ _render(renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (this.dirty !== this.fastRectDirty) - { - this.fastRectDirty = this.dirty; - this._fastRect = this.isFastRect(); - } + this.finishPoly(); - // TODO this check can be moved to dirty? - if (this._fastRect) + const geometry = this.geometry; + + // batch part.. + // batch it! + geometry.updateBatches(); + + if (geometry.batchable) { - this._renderSpriteRect(renderer); + if (geometry.batchDirty !== this.batchDirty) + { + this.batches = []; + this.batchTint = -1; + this._transformID = -1; + this.batchDirty = geometry.batchDirty; + + this.vertexData = new Float32Array(geometry.points); + + const blendMode = this.blendMode; + + for (let i = 0; i < geometry.batches.length; i++) + { + const gI = geometry.batches[i]; + + const color = gI.style.color; + + // + (alpha * 255 << 24); + + const vertexData = new Float32Array(this.vertexData.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const uvs = new Float32Array(geometry.uvsFloat32.buffer, + gI.attribStart * 4 * 2, + gI.attribSize * 2); + + const indices = new Uint16Array(geometry.indicesUint16.buffer, + gI.start * 2, + gI.size); + + const batch = { + vertexData, + blendMode, + indices, + uvs, + _batchRGB: hex2rgb(color), + _tintRGB: color, + _texture: gI.style.texture, + alpha: gI.style.alpha, + worldAlpha: 1 }; + + this.batches[i] = batch; + } + } + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + + if (this.batches.length) + { + this.calculateVertices(); + this.calculateTints(); + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.worldAlpha = this.worldAlpha * batch.alpha; + + renderer.plugins.batch.render(batch); + } + } } else { - renderer.batch.setObjectRenderer(renderer.plugins.graphics); - renderer.plugins.graphics.render(this); + // no batching... + renderer.batch.flush(); + + const s = renderer.plugins.batch.shader; + + // lets only create a shader if we need to.. + this.shader = generateMultiTextureShader(renderer.gl, renderer.plugins.batch.MAX_TEXTURES); + + if (!this.shader) + { + const sampleValues = new Int32Array(16); + + for (let i = 0; i < 16; i++) + { + sampleValues[i] = i; + } + + const uniforms = { + tint: new Float32Array([1, 1, 1]), + translationMatrix: new Matrix(), + default: UniformGroup.from({ uSamplers: sampleValues }, true), + }; + + this.shader = new Shader(s.program, uniforms); + } + + this.shader.uniforms.translationMatrix = this.transform.worldTransform;// .toArray(true); + // the first draw call, we can set the uniforms of the shader directly here. + + // this means that we can tack advantage of the sync function of pixi! + // bind and sync uniforms.. + // there is a way to optimise this.. + renderer.shader.bind(this.shader); + + // then render it + renderer.geometry.bind(geometry, this.shader); + + // set state.. + renderer.state.setState(this.state); + + // then render the rest of them... + for (let i = 0; i < geometry.drawCalls.length; i++) + { + const drawCall = geometry.drawCalls[i]; + + const groupTextureCount = drawCall.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + renderer.texture.bind(drawCall.textures[j], j); + } + + // bind the geometry... + renderer.geometry.draw(drawCall.type, drawCall.size, drawCall.start); + } } } /** - * Renders a sprite rectangle. - * - * @private - * @param {PIXI.Renderer} renderer - The renderer - */ - _renderSpriteRect(renderer) - { - const rect = this.graphicsData[0].shape; - - if (!this._spriteRect) - { - this._spriteRect = new Sprite(new Texture(Texture.WHITE)); - } - - const sprite = this._spriteRect; - const fillColor = this.graphicsData[0].fillColor; - - if (this.tint === 0xffffff) - { - sprite.tint = fillColor; - } - else if (this.tint !== this._prevRectTint || fillColor !== this._prevRectFillColor) - { - const t1 = tempColor1; - const t2 = tempColor2; - - hex2rgb(fillColor, t1); - hex2rgb(this.tint, t2); - - t1[0] *= t2[0]; - t1[1] *= t2[1]; - t1[2] *= t2[2]; - - sprite.tint = rgb2hex(t1); - - this._prevRectTint = this.tint; - this._prevRectFillColor = fillColor; - } - - sprite.alpha = this.graphicsData[0].fillAlpha; - sprite.worldAlpha = this.worldAlpha * sprite.alpha; - sprite.blendMode = this.blendMode; - - sprite._texture._frame.width = rect.width; - sprite._texture._frame.height = rect.height; - - sprite.transform.worldTransform = this.transform.worldTransform; - - sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - sprite._onAnchorUpdate(); - - sprite._render(renderer); - } - - /** * Retrieves the bounds of the graphic shape as a rectangle object * * @private */ _calculateBounds() { - if (this.boundsDirty !== this.dirty) - { - this.boundsDirty = this.dirty; - this.updateLocalBounds(); - - this.cachedSpriteDirty = true; - } - - const lb = this._localBounds; + this.finishPoly(); + const lb = this.geometry.bounds; this._bounds.addFrame(this.transform, lb.minX, lb.minY, lb.maxX, lb.maxY); } @@ -1026,214 +918,79 @@ */ containsPoint(point) { - this.worldTransform.applyInverse(point, tempPoint); + this.worldTransform.applyInverse(point, Graphics._TEMP_POINT); - const graphicsData = this.graphicsData; - - for (let i = 0; i < graphicsData.length; ++i) - { - const data = graphicsData[i]; - - if (!data.fill) - { - continue; - } - - // only deal with fills.. - if (data.shape) - { - if (data.shape.contains(tempPoint.x, tempPoint.y)) - { - if (data.holes) - { - for (let i = 0; i < data.holes.length; i++) - { - const hole = data.holes[i]; - - if (hole.contains(tempPoint.x, tempPoint.y)) - { - return false; - } - } - } - - return true; - } - } - } - - return false; + return this.geometry.containsPoint(Graphics._TEMP_POINT); } /** - * Update the bounds of the object - * + * Recalcuate the tint by applying tin to batches using Graphics tint. + * @private */ - updateLocalBounds() + calculateTints() { - let minX = Infinity; - let maxX = -Infinity; - - let minY = Infinity; - let maxY = -Infinity; - - if (this.graphicsData.length) + if (this.batchTint !== this.tint) { - let shape = 0; - let x = 0; - let y = 0; - let w = 0; - let h = 0; + this.batchTint = this.tint; - for (let i = 0; i < this.graphicsData.length; i++) + const tintRGB = hex2rgb(this.tint, temp); + + for (let i = 0; i < this.batches.length; i++) { - const data = this.graphicsData[i]; - const type = data.type; - const lineWidth = data.lineWidth; + const batch = this.batches[i]; - shape = data.shape; + const batchTint = batch._batchRGB; - if (type === SHAPES.RECT || type === SHAPES.RREC) - { - x = shape.x - (lineWidth / 2); - y = shape.y - (lineWidth / 2); - w = shape.width + lineWidth; - h = shape.height + lineWidth; + const r = (tintRGB[0] * batchTint[0]) * 255; + const g = (tintRGB[1] * batchTint[1]) * 255; + const b = (tintRGB[2] * batchTint[2]) * 255; - minX = x < minX ? x : minX; - maxX = x + w > maxX ? x + w : maxX; + // TODO Ivan, can this be done in one go? + const color = (r << 16) + (g << 8) + (b | 0); - minY = y < minY ? y : minY; - maxY = y + h > maxY ? y + h : maxY; - } - else if (type === 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 === 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 - const points = shape.points; - let x2 = 0; - let y2 = 0; - let dx = 0; - let dy = 0; - let rw = 0; - let rh = 0; - let cx = 0; - let cy = 0; - - for (let j = 0; j + 2 < points.length; j += 2) - { - x = points[j]; - y = points[j + 1]; - x2 = points[j + 2]; - y2 = points[j + 3]; - dx = Math.abs(x2 - x); - dy = Math.abs(y2 - y); - h = lineWidth; - w = Math.sqrt((dx * dx) + (dy * dy)); - - if (w < 1e-9) - { - continue; - } - - rw = ((h / w * dy) + dx) / 2; - rh = ((h / w * dx) + dy) / 2; - cx = (x2 + x) / 2; - cy = (y2 + y) / 2; - - minX = cx - rw < minX ? cx - rw : minX; - maxX = cx + rw > maxX ? cx + rw : maxX; - - minY = cy - rh < minY ? cy - rh : minY; - maxY = cy + rh > maxY ? cy + rh : maxY; - } - } + batch._tintRGB = (color >> 16) + + (color & 0xff00) + + ((color & 0xff) << 16); } } - else - { - minX = 0; - maxX = 0; - minY = 0; - maxY = 0; - } - - const padding = this.boundsPadding; - - this._localBounds.minX = minX - padding; - this._localBounds.maxX = maxX + padding; - - this._localBounds.minY = minY - padding; - this._localBounds.maxY = maxY + padding; } /** - * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. - * - * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. - * @return {PIXI.GraphicsData} The generated GraphicsData object. + * If there's a transform update or a change to the shape of the + * geometry, recaculate the vertices. + * @private */ - drawShape(shape) + calculateVertices() { - if (this.currentPath) + if (this._transformID === this.transform._worldID) { - // check current path! - if (this.currentPath.shape.points.length <= 2) - { - this.graphicsData.pop(); - } + return; } - this.currentPath = null; + this._transformID = this.transform._worldID; - const data = new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.filling, - this.nativeLines, - shape, - this.lineAlignment - ); + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; - this.graphicsData.push(data); + const data = this.geometry.points;// batch.vertexDataOriginal; + const vertexData = this.vertexData; - if (data.type === SHAPES.POLY) + let count = 0; + + // console.log('.,', data.length); + for (let i = 0; i < data.length; i += 2) { - data.shape.closed = data.shape.closed; - this.currentPath = data; + const x = data[i]; + const y = data[i + 1]; + + vertexData[count++] = (a * x) + (c * y) + tx; + vertexData[count++] = (d * y) + (b * x) + ty; } - - this.dirty++; - - return data; } /** @@ -1255,19 +1012,40 @@ } /** - * Adds a hole in the current path. + * Apply a matrix to the positional data. * + * @param {PIXI.Matrix} matrix - Matrix to use for transform current shape. * @return {PIXI.Graphics} Returns itself. */ - addHole() + setMatrix(matrix) { - // this is a hole! - const hole = this.graphicsData.pop(); + this._matrix = matrix; - this.currentPath = this.graphicsData[this.graphicsData.length - 1]; + return this; + } - this.currentPath.addHole(hole.shape); - this.currentPath = null; + /** + * Begin adding holes to the last draw shape + * IMPORTANT: holes must be fully inside a shape to work + * Also weirdness ensues if holes overlap! + * @return {PIXI.Graphics} Returns itself. + */ + beginHole() + { + this.finishPoly(); + this._holeMode = true; + + return this; + } + + /** + * End adding holes to the last draw shape + * @return {PIXI.Graphics} Returns itself. + */ + endHole() + { + this.finishPoly(); + this._holeMode = false; return this; } @@ -1283,59 +1061,39 @@ * Should it destroy the texture of the child sprite * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true * Should it destroy the base texture of the child sprite + * @param {boolean} [options.geometry=false] - if set to true, the geometry object will be + * be destroyed. */ destroy(options) { - super.destroy(options); + const destroyGeometry = typeof options === 'boolean' ? options : options && options.geometry; - // destroy each of the GraphicsData objects - for (let i = 0; i < this.graphicsData.length; ++i) + if (destroyGeometry || this._ownsGeometry) { - this.graphicsData[i].destroy(); + this.geometry.destroy(); } - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._webGL) - { - for (let j = 0; j < this._webGL[id].data.length; ++j) - { - this._webGL[id].data[j].destroy(); - } - } - - if (this._spriteRect) - { - this._spriteRect.destroy(); - } - - this.graphicsData = null; - + this._matrix = null; this.currentPath = null; - this._webGL = null; - this._localBounds = null; + this._lineStyle.destroy(); + this._lineStyle = null; + this._fillStyle.destroy(); + this._fillStyle = null; + this.geometry = null; + this.shader = null; + this.vertexData = null; + this.batches.length = 0; + this.batches = null; + + super.destroy(options); } } -Graphics._SPRITE_TEXTURE = null; - /** - * Graphics curves resolution settings. If `adaptive` flag is set to `true`, - * the resolution is calculated based on the curve's length to ensure better visual quality. - * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * Temporary point to use for containsPoint * * @static - * @constant - * @memberof PIXI.Graphics - * @name CURVES - * @type {object} - * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive - * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) - * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) - * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + * @private + * @member {PIXI.Point} */ -Graphics.CURVES = { - adaptive: false, - maxLength: 10, - minSegments: 8, - maxSegments: 2048, -}; +Graphics._TEMP_POINT = new Point(); diff --git a/packages/graphics/src/GraphicsData.js b/packages/graphics/src/GraphicsData.js index e1b592b..91598b3 100644 --- a/packages/graphics/src/GraphicsData.js +++ b/packages/graphics/src/GraphicsData.js @@ -8,94 +8,54 @@ { /** * - * @param {number} lineWidth - the width of the line to draw - * @param {number} lineColor - the color of the line to draw - * @param {number} lineAlpha - the alpha of the line to draw - * @param {number} fillColor - the color of the fill - * @param {number} fillAlpha - the alpha of the fill - * @param {boolean} fill - whether or not the shape is filled with a colour - * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. - * @param {number} lineAlignment - the alignment of the line. + * @param {PIXI.FillStyle} [fillStyle] - the width of the line to draw + * @param {PIXI.LineStyle} [lineStyle] - the color of the line to draw + * @param {PIXI.Matrix} [matrix] - Transform matrix */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape, lineAlignment) + constructor(shape, fillStyle = null, lineStyle = null, matrix = null) { /** - * the width of the line to draw - * @member {number} - */ - this.lineWidth = lineWidth; - - /** - * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). - * - * @member {number} - */ - this.lineAlignment = lineAlignment; - - /** - * if true the liens will be draw using LINES instead of TRIANGLE_STRIP - * @member {boolean} - */ - this.nativeLines = nativeLines; - - /** - * the color of the line to draw - * @member {number} - */ - this.lineColor = lineColor; - - /** - * the alpha of the line to draw - * @member {number} - */ - this.lineAlpha = lineAlpha; - - /** - * cached tint of the line to draw - * @member {number} - * @private - */ - this._lineTint = lineColor; - - /** - * the color of the fill - * @member {number} - */ - this.fillColor = fillColor; - - /** - * the alpha of the fill - * @member {number} - */ - this.fillAlpha = fillAlpha; - - /** - * cached tint of the fill - * @member {number} - * @private - */ - this._fillTint = fillColor; - - /** - * whether or not the shape is filled with a colour - * @member {boolean} - */ - this.fill = fill; - - this.holes = []; - - /** * The shape object to draw. * @member {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} */ this.shape = shape; /** + * The style of the line. + * @member {PIXI.LineStyle} + */ + this.lineStyle = lineStyle; + + /** + * The style of the fill. + * @member {PIXI.FillStyle} + */ + this.fillStyle = fillStyle; + + /** + * The transform matrix. + * @member {PIXI.Matrix} + */ + this.matrix = matrix; + + /** * The type of the shape, see the Const.Shapes file for all the existing types, * @member {number} */ this.type = shape.type; + + /** + * The collection of points. + * @member {number[]} + */ + this.points = []; + + /** + * The collection of holes. + * @member {number[]} + */ + this.holes = []; } /** @@ -106,33 +66,24 @@ clone() { return new GraphicsData( - this.lineWidth, - this.lineColor, - this.lineAlpha, - this.fillColor, - this.fillAlpha, - this.fill, - this.nativeLines, - this.shape + this.shape, + this.lineStyle, + this.fillStyle, + this.matrix ); } /** - * Adds a hole to the shape. - * - * @param {PIXI.Rectangle|PIXI.Circle} shape - The shape of the hole. - */ - addHole(shape) - { - this.holes.push(shape); - } - - /** * Destroys the Graphics data. */ destroy() { this.shape = null; + this.holes.length = 0; this.holes = null; + this.points.length = 0; + this.points = null; + this.lineStyle = null; + this.fillStyle = null; } } diff --git a/packages/graphics/src/GraphicsGeometry.js b/packages/graphics/src/GraphicsGeometry.js new file mode 100644 index 0000000..9ced2c6 --- /dev/null +++ b/packages/graphics/src/GraphicsGeometry.js @@ -0,0 +1,902 @@ +import { BatchGeometry } from '@pixi/core'; +import { Rectangle, SHAPES } from '@pixi/math'; + +import GraphicsData from './GraphicsData'; +import buildCircle from './utils/buildCircle'; +import buildLine from './utils/buildLine'; +import buildPoly from './utils/buildPoly'; +import buildRectangle from './utils/buildRectangle'; +import buildRoundedRectangle from './utils/buildRoundedRectangle'; + +const BATCH_POOL = []; +const DRAW_CALL_POOL = []; + +let TICK = 0; +/** + * Map of fill commands for each shape type. + * + * @member {Object} + * @private + */ +const fillCommands = {}; + +fillCommands[SHAPES.POLY] = buildPoly; +fillCommands[SHAPES.CIRC] = buildCircle; +fillCommands[SHAPES.ELIP] = buildCircle; +fillCommands[SHAPES.RECT] = buildRectangle; +fillCommands[SHAPES.RREC] = buildRoundedRectangle; + +/** + * The Graphics class contains methods used to draw primitive shapes such as lines, circles and + * rectangles to the display, and to color and fill them. GraphicsGeometry + * is designed to not be continually update the geometry since it's expensive + * to re-tesselate using **earcut**. Consider using {@link PIXI.Mesh} for this + * use-case, it's much faster. + * + * @class + * @extends PIXI.BatchGeometry + * @memberof PIXI + */ +export default class GraphicsGeometry extends BatchGeometry +{ + constructor() + { + super(); + + /** + * An array of points to draw + * @member {PIXI.Point[]} + * @private + */ + this.points = []; + + /** + * The collection of colors + * @member {number[]} + * @private + */ + this.colors = []; + + /** + * The UVs collection + * @member {number[]} + * @private + */ + this.uvs = []; + + /** + * The indices of the vertices + * @member {number[]} + * @private + */ + this.indices = []; + + /** + * Reference to the texture IDs. + * @member {number[]} + * @private + */ + this.textureIds = []; + + /** + * The collection of drawn shapes. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsData = []; + + /** + * Graphics data representing holes in the graphicsData. + * + * @member {PIXI.GraphicsData[]} + * @private + */ + this.graphicsDataHoles = []; + + /** + * Used to detect if the graphics object has changed. If this is set to true then the graphics + * object will be recalculated. + * + * @member {number} + * @private + */ + this.dirty = 0; + + /** + * Batches need to regenerated if the geometry is updated. + * + * @member {number} + * @private + */ + this.batchDirty = -1; + + /** + * Used to check if the cache is dirty. + * + * @member {number} + * @private + */ + this.cacheDirty = -1; + + /** + * Used to detect if we clear the graphics webGL data. + * + * @member {number} + * @default 0 + * @private + */ + this.clearDirty = 0; + + /** + * List of current draw calls drived from the batches. + * + * @member {object[]} + * @private + */ + this.drawCalls = []; + + /** + * Intermediate abstract format sent to batch system. + * Can be converted to drawCalls or to batchable objects. + * + * @member {object[]} + * @private + */ + this.batches = []; + + /** + * Index of the current last shape in the stack of calls. + * + * @member {number} + * @private + */ + this.shapeIndex = 0; + + /** + * Cached bounds. + * + * @member {PIXI.Rectangle} + * @private + */ + this._bounds = new Rectangle(); + + /** + * The bounds dirty flag. + * + * @member {number} + * @private + */ + this.boundsDirty = -1; + + /** + * Padding to add to the bounds. + * + * @member {number} + * @default 0 + */ + this.boundsPadding = 0; + } + + /** + * Get the current bounds of the graphic geometry. + * + * @member {PIXI.Rectangle} + * @readonly + */ + get bounds() + { + if (this.boundsDirty !== this.dirty) + { + this.boundsDirty = this.dirty; + this.calculateBounds(); + } + + return this._bounds; + } + + /** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {PIXI.GraphicsGeometry} This GraphicsGeometry object. Good for chaining method calls + */ + clear() + { + if (this.graphicsData.length > 0) + { + this.boundsDirty = -1; + this.dirty++; + this.clearDirty++; + this.graphicsData.length = 0; + this.shapeIndex = 0; + + this.points.length = 0; + this.colors.length = 0; + this.uvs.length = 0; + this.indices.length = 0; + + for (let i = 0; i < DRAW_CALL_POOL.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + batch.start = 0; + batch.attribStart = 0; + batch.style = null; + BATCH_POOL.push(batch); + } + + this.batches.length = 0; + } + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.FillStyle} fillStyle - Defines style of the fill. + * @param {PIXI.LineStyle} lineStyle - Defines style of the lines. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawShape(shape, fillStyle, lineStyle, matrix) + { + const data = new GraphicsData(shape, fillStyle, lineStyle, matrix); + + this.graphicsData.push(data); + this.dirty++; + + return this; + } + + /** + * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. + * + * @param {PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.Rectangle|PIXI.RoundedRectangle} shape - The shape object to draw. + * @param {PIXI.Matrix} matrix - Transform applied to the points of the shape. + * @return {PIXI.GraphicsGeomery} Returns geometry for chaining. + */ + drawHole(shape, matrix) + { + if (!this.graphicsData.length) + { + return null; + } + + const data = new GraphicsData(shape, null, null, matrix); + + const lastShape = this.graphicsData[this.graphicsData.length - 1]; + + lastShape.holes.push(data); + + this.dirty++; + + return data; + } + + /** + * Destroys the Graphics object. + * + * @param {object|boolean} [options] - Options parameter. A boolean will act as if all + * options have been set to that value + * @param {boolean} [options.children=false] - if set to true, all the children will have + * their destroy method called as well. 'options' will be passed on to those calls. + * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the texture of the child sprite + * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true + * Should it destroy the base texture of the child sprite + */ + destroy(options) + { + super.destroy(options); + + // destroy each of the GraphicsData objects + for (let i = 0; i < this.graphicsData.length; ++i) + { + this.graphicsData[i].destroy(); + } + + this.points.length = 0; + this.points = null; + this.colors.length = 0; + this.colors = null; + this.uvs.length = 0; + this.uvs = null; + this.indices.length = 0; + this.indices = null; + this.indexBuffer.destroy(); + this.indexBuffer = null; + this.graphicsData.length = 0; + this.graphicsData = null; + this.graphicsDataHoles.length = 0; + this.graphicsDataHoles = null; + this.drawCalls.length = 0; + this.drawCalls = null; + this.batches.length = 0; + this.batches = null; + this._bounds = null; + } + + /** + * Check to see if a point is contained within this geometry. + * + * @param {PIXI.Point} point - Point to check if it's contained. + * @return {Boolean} `true` if the point is contained within geometry. + */ + containsPoint(point) + { + const graphicsData = this.graphicsData; + + for (let i = 0; i < graphicsData.length; ++i) + { + const data = graphicsData[i]; + + if (!data.fillStyle.visible) + { + continue; + } + + // only deal with fills.. + if (data.shape) + { + if (data.shape.contains(point.x, point.y)) + { + if (data.holes) + { + for (let i = 0; i < data.holes.length; i++) + { + const hole = data.holes[i]; + + if (hole.shape.contains(point.x, point.y)) + { + return false; + } + } + } + + return true; + } + } + } + + return false; + } + + /** + * Generates intermediate batch data. Either gets converted to drawCalls + * or used to convert to batch objects directly by the Graphics object. + * @private + */ + updateBatches() + { + if (this.dirty === this.cacheDirty) return; + if (this.graphicsData.length === 0) return; + + if (this.dirty !== this.cacheDirty) + { + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + if (data.fillStyle && !data.fillStyle.texture.baseTexture.valid) return; + if (data.lineStyle && !data.lineStyle.texture.baseTexture.valid) return; + } + } + + this.cacheDirty = this.dirty; + + const uvs = this.uvs; + + let batchPart = this.batches.pop() + || BATCH_POOL.pop() + || { style: null, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + + batchPart.style = batchPart.style + || this.graphicsData[0].fillStyle + || this.graphicsData[0].lineStyle; + + let currentTexture = batchPart.style.texture.baseTexture; + let currentColor = batchPart.style.color + batchPart.style.alpha; + + this.batches.push(batchPart); + + // TODO - this can be simplified + for (let i = this.shapeIndex; i < this.graphicsData.length; i++) + { + this.shapeIndex++; + + const data = this.graphicsData[i]; + const command = fillCommands[data.type]; + + const fillStyle = data.fillStyle; + const lineStyle = data.lineStyle; + + // build out the shapes points.. + command.build(data); + + if (data.matrix) + { + this.transformPoints(data.points, data.matrix); + } + + for (let j = 0; j < 2; j++) + { + const style = (j === 0) ? fillStyle : lineStyle; + + if (!style.visible) continue; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture || (style.color + style.alpha) !== currentColor) + { + // TODO use a const + nextTexture.wrapMode = 10497; + currentTexture = nextTexture; + currentColor = style.color + style.alpha; + + const index = this.indices.length; + const attribIndex = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attribIndex - batchPart.attribStart; + + if (batchPart.size > 0) + { + batchPart = BATCH_POOL.pop() || { style, size: 0, start: 0, attribStart: 0, attribSize: 0 }; + this.batches.push(batchPart); + } + + batchPart.style = style; + batchPart.start = index; + batchPart.attribStart = attribIndex; + + // TODO add this to the render part.. + } + + const start = this.points.length / 2; + + if (j === 0) + { + if (data.holes.length) + { + this.proccessHoles(data.holes); + + buildPoly.triangulate(data, this); + } + else + { + command.triangulate(data, this); + } + } + else + { + buildLine(data, this); + } + + const size = (this.points.length / 2) - start; + + this.addUvs(this.points, uvs, style.texture, start, size, style.matrix); + } + } + + const index = this.indices.length; + const attrib = this.points.length / 2; + + batchPart.size = index - batchPart.start; + batchPart.attribSize = attrib - batchPart.attribStart; + this.indicesUint16 = new Uint16Array(this.indices); + + // TODO make this a const.. + this.batchable = this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2; + + if (this.batchable) + { + this.batchDirty++; + + this.uvsFloat32 = new Float32Array(this.uvs); + + // offset the indices so that it works with the batcher... + for (let i = 0; i < this.batches.length; i++) + { + const batch = this.batches[i]; + + for (let j = 0; j < batch.size; j++) + { + const index = batch.start + j; + + this.indicesUint16[index] = this.indicesUint16[index] - batch.attribStart; + } + } + } + else + { + this.buildDrawCalls(); + } + } + + /** + * Converts intermediate batches data to drawCalls. + * @private + */ + buildDrawCalls() + { + TICK++; + + for (let i = 0; i < this.drawCalls.length; i++) + { + DRAW_CALL_POOL.push(this.drawCalls[i]); + } + + this.drawCalls.length = 0; + + let lastIndex = this.indices.length; + + const uvs = this.uvs; + const colors = this.colors; + const textureIds = this.textureIds; + + let currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + currentGroup.start = 0; + + let textureCount = 0; + let currentTexture = null; + let textureId = 0; + + lastIndex = 0; + + this.drawCalls.push(currentGroup); + + // TODO - this can be simplified + for (let i = 0; i < this.batches.length; i++) + { + const data = this.batches[i]; + + // TODO add some full on MAX_TEXTURE CODE.. + const MAX_TEXTURES = 8; + + const style = data.style; + + const nextTexture = style.texture.baseTexture; + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._enabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + textureCount = 0; + + const index = data.start; + + currentGroup.size = index - lastIndex; + + currentGroup = DRAW_CALL_POOL.pop() || { textures: [], textureCount: 0, size: 0, start: 0, type: 4 }; + + currentGroup.textureCount = 0; + + currentGroup.start = lastIndex; + this.drawCalls.push(currentGroup); + + lastIndex = index; + } + + // TODO add this to the render part.. + nextTexture.touched = 1;// touch; + nextTexture._enabled = TICK; + nextTexture._id = textureCount; + nextTexture.wrapMode = 10497; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + const size = data.attribSize; + + textureId = nextTexture._id; + + this.addColors(colors, style.color, style.alpha, size); + this.addTextureIds(textureIds, textureId, size); + } + + const index = this.indices.length; + + currentGroup.size = index - lastIndex; + + // upload.. + // merge for now! + const verts = this.points; + + // verts are 2 positions.. so we * by 3 as there are 6 properties.. then 4 cos its bytes + const glPoints = new ArrayBuffer(verts.length * 3 * 4); + const f32 = new Float32Array(glPoints); + const u32 = new Uint32Array(glPoints); + + let p = 0; + + for (let i = 0; i < verts.length / 2; i++) + { + f32[p++] = verts[i * 2]; + f32[p++] = verts[(i * 2) + 1]; + + f32[p++] = uvs[i * 2]; + f32[p++] = uvs[(i * 2) + 1]; + + u32[p++] = colors[i]; + + f32[p++] = textureIds[i]; + } + + this._buffer.update(glPoints); + this._indexBuffer.update(this.indicesUint16); + } + + /** + * Process the holes data. + * + * @param {PIXI.GraphicsData[]} holes - Holes to render + * @private + */ + proccessHoles(holes) + { + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; + + const command = fillCommands[hole.type]; + + command.build(hole); + + if (hole.matrix) + { + this.transformPoints(hole.points, hole.matrix); + } + } + } + + /** + * Update the local bounds of the object. Expensive to use performance-wise. + * @private + */ + calculateBounds() + { + let minX = Infinity; + let maxX = -Infinity; + + let minY = Infinity; + let maxY = -Infinity; + + if (this.graphicsData.length) + { + let shape = 0; + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + for (let i = 0; i < this.graphicsData.length; i++) + { + const data = this.graphicsData[i]; + + const type = data.type; + const lineWidth = data.lineStyle ? data.lineStyle.width : 0; + + shape = data.shape; + + if (type === SHAPES.RECT || type === 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 === 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 === 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 + const points = shape.points; + let x2 = 0; + let y2 = 0; + let dx = 0; + let dy = 0; + let rw = 0; + let rh = 0; + let cx = 0; + let cy = 0; + + for (let j = 0; j + 2 < points.length; j += 2) + { + x = points[j]; + y = points[j + 1]; + x2 = points[j + 2]; + y2 = points[j + 3]; + dx = Math.abs(x2 - x); + dy = Math.abs(y2 - y); + h = lineWidth; + w = Math.sqrt((dx * dx) + (dy * dy)); + + if (w < 1e-9) + { + continue; + } + + rw = ((h / w * dy) + dx) / 2; + rh = ((h / w * dx) + dy) / 2; + cx = (x2 + x) / 2; + cy = (y2 + y) / 2; + + minX = cx - rw < minX ? cx - rw : minX; + maxX = cx + rw > maxX ? cx + rw : maxX; + + minY = cy - rh < minY ? cy - rh : minY; + maxY = cy + rh > maxY ? cy + rh : maxY; + } + } + } + } + else + { + minX = 0; + maxX = 0; + minY = 0; + maxY = 0; + } + + const padding = this.boundsPadding; + + this._bounds.minX = minX - padding; + this._bounds.maxX = maxX + padding; + + this._bounds.minY = minY - padding; + this._bounds.maxY = maxY + padding; + } + + /** + * Transform points using matrix. + * + * @private + * @param {number[]} points - Points to transform + * @param {PIXI.Matrix} matrix - Transform matrix + */ + transformPoints(points, matrix) + { + for (let i = 0; i < points.length / 2; i++) + { + const x = points[(i * 2)]; + const y = points[(i * 2) + 1]; + + points[(i * 2)] = (matrix.a * x) + (matrix.c * y) + matrix.tx; + points[(i * 2) + 1] = (matrix.b * x) + (matrix.d * y) + matrix.ty; + } + } + + /** + * Add colors. + * + * @private + * @param {number[]} colors - List of colors to add to + * @param {number} color - Color to add + * @param {number} alpha - Alpha to use + * @param {number} size - Number of colors to add + */ + addColors(colors, color, alpha, size) + { + // TODO use the premultiply bits Ivan added + const tRGB = ((color >> 16) * alpha) + + ((color & 0xff00) * alpha) + + (((color & 0xff) << 16) * alpha) + + (alpha * 255 << 24); + + while (size-- > 0) + { + colors.push(tRGB); + } + } + + /** + * Add texture id that the shader/fragment wants to use. + * + * @private + * @param {number[]} textureIds + * @param {number} id + * @param {number} size + */ + addTextureIds(textureIds, id, size) + { + while (size-- > 0) + { + textureIds.push(id); + } + } + + /** + * Generates the UVs for a shape. + * + * @private + * @param {number[]} verts - Vertices + * @param {number[]} uvs - UVs + * @param {PIXI.Texture} texture - Reference to Texture + * @param {number} start - Index buffer start index. + * @param {number} size - The size/length for index buffer. + * @param {PIXI.Matrix} [matrix] - Optional transform for all points. + */ + addUvs(verts, uvs, texture, start, size, matrix) + { + let index = 0; + + while (index < size) + { + let x = verts[(start + index) * 2]; + let y = verts[((start + index) * 2) + 1]; + + if (matrix) + { + const nx = (matrix.a * x) + (matrix.c * y) + matrix.tx; + + y = (matrix.b * x) + (matrix.d * y) + matrix.ty; + x = nx; + } + + index++; + + const frame = texture.frame; + + uvs.push(x / frame.width, y / frame.height); + } + } +} + +/** + * The maximum number of points to consider an object "batchable", + * able to be batched by the renderer's batch system. + * + * @memberof PIXI.GraphicsGeometry + * @static + * @member {number} + * @default 100 + */ +GraphicsGeometry.BATCHABLE_SIZE = 100; diff --git a/packages/graphics/src/GraphicsRenderer.js b/packages/graphics/src/GraphicsRenderer.js deleted file mode 100644 index 7dcee4e..0000000 --- a/packages/graphics/src/GraphicsRenderer.js +++ /dev/null @@ -1,235 +0,0 @@ -import { hex2rgb } from '@pixi/utils'; -import { SHAPES } from '@pixi/math'; -import { ObjectRenderer } from '@pixi/core'; - -import WebGLGraphicsData from './WebGLGraphicsData'; -import PrimitiveShader from './shaders/PrimitiveShader'; - -import buildPoly from './utils/buildPoly'; -import buildRectangle from './utils/buildRectangle'; -import buildRoundedRectangle from './utils/buildRoundedRectangle'; -import buildCircle from './utils/buildCircle'; - -/** - * Renders the graphics object. - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class GraphicsRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this object renderer works for. - */ - constructor(renderer) - { - super(renderer); - - this.graphicsDataPool = []; - - this.primitiveShader = new PrimitiveShader(); - this.primitiveShader.uniforms.globals = renderer.globalUniforms; - this.gl = renderer.gl; - - // easy access! - this.CONTEXT_UID = 0; - } - - /** - * Called when there is a WebGL context change - * - * @private - * - */ - onContextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * Destroys this renderer. - * - */ - destroy() - { - ObjectRenderer.prototype.destroy.call(this); - - for (let i = 0; i < this.graphicsDataPool.length; ++i) - { - this.graphicsDataPool[i].destroy(); - } - - this.graphicsDataPool = null; - } - - /** - * Renders a graphics object. - * - * @param {PIXI.Graphics} graphics - The graphics object to render. - */ - render(graphics) - { - const renderer = this.renderer; - const gl = renderer.gl; - - let webGLData; - let webGL = graphics._webGL[this.CONTEXT_UID]; - - if (!webGL || graphics.dirty !== webGL.dirty) - { - this.updateGraphics(graphics); - - webGL = graphics._webGL[this.CONTEXT_UID]; - } - - // This could be speeded up for sure! - const shader = this.primitiveShader; - - renderer.state.setBlendMode(graphics.blendMode); - - for (let i = 0, n = webGL.data.length; i < n; i++) - { - webGLData = webGL.data[i]; - - shader.uniforms.translationMatrix = graphics.transform.worldTransform.toArray(true); - shader.uniforms.tint = hex2rgb(graphics.tint); - shader.uniforms.alpha = graphics.worldAlpha; - - renderer.shader.bind(shader); - renderer.geometry.bind(webGLData.geometry); - - if (webGLData.nativeLines) - { - renderer.geometry.draw(gl.LINES, webGLData.indices.length / 6); - } - else - { - renderer.geometry.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); - } - } - } - - /** - * Updates the graphics object - * - * @private - * @param {PIXI.Graphics} graphics - The graphics object to update - */ - updateGraphics(graphics) - { - const gl = this.renderer.gl; - - // get the contexts graphics object - let webGL = graphics._webGL[this.CONTEXT_UID]; - - // if the graphics object does not exist in the webGL context time to create it! - if (!webGL) - { - webGL = graphics._webGL[this.CONTEXT_UID] = { lastIndex: 0, data: [], gl, clearDirty: -1, dirty: -1 }; - } - - // flag the graphics as not dirty as we are about to update it... - webGL.dirty = graphics.dirty; - - // if the user cleared the graphics object we will need to clear every object - if (graphics.clearDirty !== webGL.clearDirty) - { - webGL.clearDirty = graphics.clearDirty; - - // loop through and return all the webGLDatas to the object pool so than can be reused later on - for (let i = 0; i < webGL.data.length; i++) - { - this.graphicsDataPool.push(webGL.data[i]); - } - - // clear the array and reset the index.. - webGL.data.length = 0; - webGL.lastIndex = 0; - } - - let webGLData; - let webGLDataNativeLines; - - // loop through the graphics datas and construct each one.. - // if the object is a complex fill then the new stencil buffer technique will be used - // other wise graphics objects will be pushed into a batch.. - for (let i = webGL.lastIndex; i < graphics.graphicsData.length; i++) - { - const data = graphics.graphicsData[i]; - - // TODO - this can be simplified - webGLData = this.getWebGLData(webGL, 0); - - if (data.nativeLines && data.lineWidth) - { - webGLDataNativeLines = this.getWebGLData(webGL, 0, true); - webGL.lastIndex++; - } - - if (data.type === SHAPES.POLY) - { - buildPoly(data, webGLData, webGLDataNativeLines); - } - if (data.type === SHAPES.RECT) - { - buildRectangle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.CIRC || data.type === SHAPES.ELIP) - { - buildCircle(data, webGLData, webGLDataNativeLines); - } - else if (data.type === SHAPES.RREC) - { - buildRoundedRectangle(data, webGLData, webGLDataNativeLines); - } - - webGL.lastIndex++; - } - - // this.renderer.geometry.bindVao(null); - - // upload all the dirty data... - for (let i = 0; i < webGL.data.length; i++) - { - webGLData = webGL.data[i]; - - if (webGLData.dirty) - { - webGLData.upload(); - } - } - } - - /** - * - * @private - * @param {WebGLRenderingContext} gl - the current WebGL drawing context - * @param {number} type - TODO @Alvin - * @param {number} nativeLines - indicate whether the webGLData use for nativeLines. - * @return {*} TODO - */ - getWebGLData(gl, type, nativeLines) - { - let webGLData = gl.data[gl.data.length - 1]; - - if (!webGLData || webGLData.nativeLines !== nativeLines || webGLData.points.length > 320000) - { - webGLData = this.graphicsDataPool.pop() || new WebGLGraphicsData( - this.renderer.gl, - this.primitiveShader, - this.renderer.state.attribsState - ); - - webGLData.nativeLines = nativeLines; - - webGLData.reset(type); - gl.data.push(webGLData); - } - - webGLData.dirty = true; - - return webGLData; - } -} diff --git a/packages/graphics/src/WebGLGraphicsData.js b/packages/graphics/src/WebGLGraphicsData.js deleted file mode 100644 index 7361879..0000000 --- a/packages/graphics/src/WebGLGraphicsData.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Geometry, Buffer } from '@pixi/core'; - -/** - * An object containing WebGL specific properties to be used by the WebGL renderer - * - * @class - * @private - * @memberof PIXI - */ -export default class WebGLGraphicsData -{ - /** - * @param {WebGLRenderingContext} gl - The current WebGL drawing context - * @param {PIXI.Shader} shader - The shader - */ - constructor(gl, shader) - { - /** - * The current WebGL drawing context - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - // TODO does this need to be split before uploading?? - /** - * An array of color components (r,g,b) - * @member {number[]} - */ - this.color = [0, 0, 0]; // color split! - - /** - * An array of points to draw - * @member {PIXI.Point[]} - */ - this.points = []; - - /** - * The indices of the vertices - * @member {number[]} - */ - this.indices = []; - /** - * The main buffer - * @member {WebGLBuffer} - */ - this.buffer = new Buffer(); - - /** - * The index buffer - * @member {WebGLBuffer} - */ - this.indexBuffer = new Buffer(); - - /** - * Whether this graphics is dirty or not - * @member {boolean} - */ - this.dirty = true; - - /** - * Whether this graphics is nativeLines or not - * @member {boolean} - */ - this.nativeLines = false; - - this.glPoints = null; - this.glIndices = null; - - /** - * - * @member {PIXI.Shader} - */ - this.shader = shader; - - this.geometry = new Geometry() - .addAttribute('aVertexPosition|aColor', this.buffer) - .addIndex(this.indexBuffer); - } - - /** - * Resets the vertices and the indices - */ - reset() - { - this.points.length = 0; - this.indices.length = 0; - } - - /** - * Binds the buffers and uploads the data - */ - upload() - { - this.glPoints = new Float32Array(this.points); - this.buffer.update(this.glPoints); - - this.glIndices = new Uint16Array(this.indices); - this.indexBuffer.update(this.glIndices); - - // console.log("UPLOADING,.",this.glPoints,this.glIndices) - this.dirty = false; - } - - /** - * Empties all the data - */ - destroy() - { - this.color = null; - this.points = null; - this.indices = null; - - this.vao.destroy(); - this.buffer.destroy(); - this.indexBuffer.destroy(); - - this.gl = null; - - this.buffer = null; - this.indexBuffer = null; - - this.glPoints = null; - this.glIndices = null; - } -} diff --git a/packages/graphics/src/const.js b/packages/graphics/src/const.js new file mode 100644 index 0000000..34679d6 --- /dev/null +++ b/packages/graphics/src/const.js @@ -0,0 +1,41 @@ +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI + * @name GRAPHICS_CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +export const GRAPHICS_CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, + _segmentsCount(length, defaultSegments = 20) + { + if (this.adaptive) + { + return defaultSegments; + } + + let result = Math.ceil(length / this.maxLength); + + if (result < this.minSegments) + { + result = this.minSegments; + } + else if (result > this.maxSegments) + { + result = this.maxSegments; + } + + return result; + }, +}; diff --git a/packages/graphics/src/index.js b/packages/graphics/src/index.js index 1946f19..7b8f531 100644 --- a/packages/graphics/src/index.js +++ b/packages/graphics/src/index.js @@ -1,3 +1,4 @@ +export * from './const'; export { default as Graphics } from './Graphics'; export { default as GraphicsData } from './GraphicsData'; -export { default as GraphicsRenderer } from './GraphicsRenderer'; +export { default as GraphicsGeometry } from './GraphicsGeometry'; diff --git a/packages/graphics/src/shaders/PrimitiveShader.js b/packages/graphics/src/shaders/PrimitiveShader.js deleted file mode 100644 index 13268d9..0000000 --- a/packages/graphics/src/shaders/PrimitiveShader.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Shader, Program } from '@pixi/core'; -import fragment from './primitive.frag'; -import vertex from './primitive.vert'; - -/** - * This shader is used to draw simple primitive shapes for {@link PIXI.Graphics}. - * - * @class - * @memberof PIXI - * @extends PIXI.Shader - */ -export default class PrimitiveShader extends Shader -{ - /** - * @param {WebGLRenderingContext} gl - The webgl shader manager this shader works for. - */ - constructor() - { - const program = Program.from(vertex, fragment); - - super(program, {}); - } -} diff --git a/packages/graphics/src/shaders/primitive.frag b/packages/graphics/src/shaders/primitive.frag deleted file mode 100644 index 052a521..0000000 --- a/packages/graphics/src/shaders/primitive.frag +++ /dev/null @@ -1,5 +0,0 @@ -varying vec4 vColor; - -void main(void){ - gl_FragColor = vColor; -} diff --git a/packages/graphics/src/shaders/primitive.vert b/packages/graphics/src/shaders/primitive.vert deleted file mode 100644 index cce8c1e..0000000 --- a/packages/graphics/src/shaders/primitive.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec4 aColor; - -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -uniform float alpha; -uniform vec3 tint; - -varying vec4 vColor; - -void main(void){ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vColor = aColor * vec4(tint * alpha, alpha); -} diff --git a/packages/graphics/src/styles/FillStyle.js b/packages/graphics/src/styles/FillStyle.js new file mode 100644 index 0000000..14cc66e --- /dev/null +++ b/packages/graphics/src/styles/FillStyle.js @@ -0,0 +1,85 @@ +import { Texture } from '@pixi/core'; + +/** + * Fill style object for Graphics. + * @class + * @memberof PIXI + */ +export default class FillStyle +{ + constructor() + { + this.reset(); + } + + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + }; + } + + /** + * Reset + */ + reset() + { + /** + * The hex color value used when coloring the Graphics object. + * + * @member {number} + * @default 1 + */ + this.color = 0xFFFFFF; + + /** + * The alpha value used when filling the Graphics object. + * + * @member {number} + * @default 1 + */ + this.alpha = 1; + + /** + * The texture to be used for the fill. + * + * @member {string} + * @default 0 + */ + this.texture = Texture.WHITE; + + /** + * The transform aplpied to the texture. + * + * @member {string} + * @default 0 + */ + this.matrix = null; + + /** + * If the current fill is visible. + * + * @member {boolean} + * @default false + */ + this.visible = false; + } + + /** + * Destroy and don't use after this + */ + destroy() + { + this.texture = null; + this.matrix = null; + } +} diff --git a/packages/graphics/src/styles/LineStyle.js b/packages/graphics/src/styles/LineStyle.js new file mode 100644 index 0000000..32a36e7 --- /dev/null +++ b/packages/graphics/src/styles/LineStyle.js @@ -0,0 +1,64 @@ +import FillStyle from './FillStyle'; + +/** + * Represents the line style for Graphics. + * @memberof PIXI + * @class + * @extends PIXI.FillStyle + */ +export default class LineStyle extends FillStyle +{ + /** + * Convert the object to JSON + * + * @return {object} + */ + toJSON() + { + return { + color: this.color, + alpha: this.alpha, + texture: this.texture, + matrix: this.matrix, + visible: this.visible, + width: this.width, + alignment: this.alignment, + native: this.native, + }; + } + + /** + * Reset the line style to default. + */ + reset() + { + super.reset(); + + // Override default line style color + this.color = 0x0; + + /** + * The width (thickness) of any lines drawn. + * + * @member {number} + * @default 0 + */ + this.width = 0; + + /** + * The alignment of any lines drawn (0.5 = middle, 1 = outter, 0 = inner). + * + * @member {number} + * @default 0 + */ + this.alignment = 0.5; + + /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + * @default false + */ + this.native = false; + } +} diff --git a/packages/graphics/src/utils/ArcUtils.js b/packages/graphics/src/utils/ArcUtils.js new file mode 100644 index 0000000..27bf365 --- /dev/null +++ b/packages/graphics/src/utils/ArcUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; +import { PI_2 } from '@pixi/math'; + +/** + * Utilities for arc curves + * @class + * @private + */ +export default class ArcUtils +{ + /** + * The arcTo() method creates an arc/curve between two tangents on the canvas. + * + * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! + * + * @param {number} x1 - The x-coordinate of the beginning of the arc + * @param {number} y1 - The y-coordinate of the beginning of the arc + * @param {number} x2 - The x-coordinate of the end of the arc + * @param {number} y2 - The y-coordinate of the end of the arc + * @param {number} radius - The radius of the arc + * @return {object} If the arc length is valid, return center of circle, radius and other info otherwise `null`. + */ + static curveTo(x1, y1, x2, y2, radius, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const a1 = fromY - y1; + const b1 = fromX - x1; + const a2 = y2 - y1; + const b2 = x2 - x1; + const 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); + } + + return null; + } + + const dd = (a1 * a1) + (b1 * b1); + const cc = (a2 * a2) + (b2 * b2); + const tt = (a1 * a2) + (b1 * b2); + const k1 = radius * Math.sqrt(dd) / mm; + const k2 = radius * Math.sqrt(cc) / mm; + const j1 = k1 * tt / dd; + const j2 = k2 * tt / cc; + const cx = (k1 * b2) + (k2 * b1); + const cy = (k1 * a2) + (k2 * a1); + const px = b1 * (k2 + j1); + const py = a1 * (k2 + j1); + const qx = b2 * (k1 + j2); + const qy = a2 * (k1 + j2); + const startAngle = Math.atan2(py - cy, px - cx); + const endAngle = Math.atan2(qy - cy, qx - cx); + + return { + cx: (cx + x1), + cy: (cy + y1), + radius, + startAngle, + endAngle, + anticlockwise: (b1 * a2 > b2 * a1), + }; + } + + /** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @param {number} startX - Start x location of arc + * @param {number} startY - Start y location of arc + * @param {number} cx - The x-coordinate of the center of the circle + * @param {number} cy - The y-coordinate of the center of the circle + * @param {number} radius - The radius of the circle + * @param {number} startAngle - The starting angle, in radians (0 is at the 3 o'clock position + * of the arc's circle) + * @param {number} endAngle - The ending angle, in radians + * @param {boolean} anticlockwise - Specifies whether the drawing should be + * counter-clockwise or clockwise. False is default, and indicates clockwise, while true + * indicates counter-clockwise. + * @param {number} n - Number of segments + * @param {number[]} points - Collection of points to add to + */ + static arc(startX, startY, cx, cy, radius, startAngle, endAngle, anticlockwise, points) + { + const sweep = endAngle - startAngle; + const n = GRAPHICS_CURVES._segmentsCount( + Math.abs(sweep) * radius, + Math.ceil(Math.abs(sweep) / PI_2) * 40 + ); + + const theta = (sweep) / (n * 2); + const theta2 = theta * 2; + const cTheta = Math.cos(theta); + const sTheta = Math.sin(theta); + const segMinus = n - 1; + const remainder = (segMinus % 1) / segMinus; + + for (let i = 0; i <= segMinus; ++i) + { + const real = i + (remainder * i); + const angle = ((theta) + startAngle + (theta2 * real)); + const c = Math.cos(angle); + const s = -Math.sin(angle); + + points.push( + (((cTheta * c) + (sTheta * s)) * radius) + cx, + (((cTheta * -s) + (sTheta * c)) * radius) + cy + ); + } + } +} diff --git a/packages/graphics/src/utils/BezierUtils.js b/packages/graphics/src/utils/BezierUtils.js new file mode 100644 index 0000000..eb78ee5 --- /dev/null +++ b/packages/graphics/src/utils/BezierUtils.js @@ -0,0 +1,115 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for bezier curves + * @class + * @private + */ +export default class BezierUtils +{ + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + static curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate the points for a bezier curve and then draws it. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Path array to push points into + */ + static curveTo(cpX, cpY, cpX2, cpY2, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + points.length -= 2; + + const n = GRAPHICS_CURVES._segmentsCount( + BezierUtils.curveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + ); + + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + + points.push(fromX, fromY); + + for (let i = 1, j = 0; 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) + ); + } + } +} diff --git a/packages/graphics/src/utils/QuadraticUtils.js b/packages/graphics/src/utils/QuadraticUtils.js new file mode 100644 index 0000000..44ca29b --- /dev/null +++ b/packages/graphics/src/utils/QuadraticUtils.js @@ -0,0 +1,83 @@ +import { GRAPHICS_CURVES } from '../const'; + +/** + * Utilities for quadratic curves + * @class + * @private + */ +export default class QuadraticUtils +{ + /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + static curveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) / (4.0 * a32); + } + + /** + * Calculate the points for a quadratic bezier curve and then draws it. + * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c + * + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @param {number[]} points - Points to add segments to. + */ + static curveTo(cpX, cpY, toX, toY, points) + { + const fromX = points[points.length - 2]; + const fromY = points[points.length - 1]; + + const n = GRAPHICS_CURVES._segmentsCount( + QuadraticUtils.curveLength(fromX, fromY, cpX, cpY, toX, toY) + ); + + let xa = 0; + let ya = 0; + + for (let i = 1; i <= n; ++i) + { + const 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)); + } + } +} diff --git a/packages/graphics/src/utils/Star.js b/packages/graphics/src/utils/Star.js new file mode 100644 index 0000000..5baf68a --- /dev/null +++ b/packages/graphics/src/utils/Star.js @@ -0,0 +1,40 @@ +import { Polygon, PI_2 } from '@pixi/math'; + +/** + * Draw a star shape with an arbitrary number of points. + * + * @class + * @extends PIXI.Polygon + * @param {number} x - Center X position of the star + * @param {number} y - Center Y position of the star + * @param {number} points - The number of points of the star, must be > 1 + * @param {number} radius - The outer radius of the star + * @param {number} [innerRadius] - The inner radius between points, default half `radius` + * @param {number} [rotation=0] - The rotation of the star in radians, where 0 is vertical + * @return {PIXI.Graphics} This Graphics object. Good for chaining method calls + */ +export default class Star extends Polygon +{ + constructor(x, y, points, radius, innerRadius, rotation) + { + innerRadius = innerRadius || radius / 2; + + const startAngle = (-1 * Math.PI / 2) + rotation; + const len = points * 2; + const delta = PI_2 / len; + const polygon = []; + + for (let i = 0; i < len; i++) + { + const r = i % 2 ? innerRadius : radius; + const angle = (i * delta) + startAngle; + + polygon.push( + x + (r * Math.cos(angle)), + y + (r * Math.sin(angle)) + ); + } + + super(polygon); + } +} diff --git a/packages/graphics/src/utils/buildCircle.js b/packages/graphics/src/utils/buildCircle.js index 8feb1ee..793b278 100644 --- a/packages/graphics/src/utils/buildCircle.js +++ b/packages/graphics/src/utils/buildCircle.js @@ -1,6 +1,4 @@ -import buildLine from './buildLine'; import { SHAPES } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a circle to draw @@ -13,90 +11,75 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildCircle(graphicsData, webGLData, webGLDataNativeLines) -{ - // need to convert points to a nice regular data - const circleData = graphicsData.shape; - const x = circleData.x; - const y = circleData.y; - let width; - let height; +export default { - // TODO - bit hacky?? - if (graphicsData.type === SHAPES.CIRC) + build(graphicsData) { - width = circleData.radius; - height = circleData.radius; - } - else - { - width = circleData.width; - height = circleData.height; - } + // need to convert points to a nice regular data + const circleData = graphicsData.shape; + const points = graphicsData.points; + const x = circleData.x; + const y = circleData.y; + let width; + let height; - if (width === 0 || height === 0) - { - return; - } + points.length = 0; - const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) - || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - - const seg = (Math.PI * 2) / totalSegs; - - if (graphicsData.fill) - { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const verts = webGLData.points; - const indices = webGLData.indices; - - let vecPos = verts.length / 6; - - indices.push(vecPos); - - for (let i = 0; i < totalSegs + 1; i++) + // TODO - bit hacky?? + if (graphicsData.type === SHAPES.CIRC) { - verts.push(x, y, r, g, b, alpha); - - verts.push( - x + (Math.sin(seg * i) * width), - y + (Math.cos(seg * i) * height), - r, g, b, alpha - ); - - indices.push(vecPos++, vecPos++); + width = circleData.radius; + height = circleData.radius; + } + else + { + width = circleData.width; + height = circleData.height; } - indices.push(vecPos - 1); - } + if (width === 0 || height === 0) + { + return; + } - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; + let totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) + || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); - graphicsData.points = []; + totalSegs /= 2.3; + + const seg = (Math.PI * 2) / totalSegs; for (let i = 0; i < totalSegs; i++) { - graphicsData.points.push( - x + (Math.sin(seg * -i) * width), - y + (Math.cos(seg * -i) * height) + points.push( + x + (Math.sin(seg * i) * width), + y + (Math.cos(seg * i) * height) ); } - graphicsData.points.push( - graphicsData.points[0], - graphicsData.points[1] + points.push( + points[0], + points[1] ); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - graphicsData.points = tempPoints; - } -} + let vertPos = verts.length / 2; + const center = vertPos; + + verts.push(graphicsData.shape.x, graphicsData.shape.y); + + for (let i = 0; i < points.length; i += 2) + { + verts.push(points[i], points[i + 1]); + + // add some uvs + indices.push(vertPos++, center, vertPos); + } + }, +}; diff --git a/packages/graphics/src/utils/buildLine.js b/packages/graphics/src/utils/buildLine.js index d2f329d..ac43591 100644 --- a/packages/graphics/src/utils/buildLine.js +++ b/packages/graphics/src/utils/buildLine.js @@ -1,5 +1,4 @@ import { Point } from '@pixi/math'; -import { hex2rgb } from '@pixi/utils'; /** * Builds a line to draw @@ -12,15 +11,15 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function (graphicsData, webGLData, webGLDataNativeLines) +export default function (graphicsData, graphicsGeometry) { - if (graphicsData.nativeLines) + if (graphicsData.lineStyle.native) { - buildNativeLine(graphicsData, webGLDataNativeLines); + buildNativeLine(graphicsData, graphicsGeometry); } else { - buildLine(graphicsData, webGLData); + buildLine(graphicsData, graphicsGeometry); } } @@ -34,10 +33,10 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildLine(graphicsData, webGLData) +function buildLine(graphicsData, graphicsGeometry) { // TODO OPTIMISE! - let points = graphicsData.points; + let points = graphicsData.points || graphicsData.shape.points.slice(); if (points.length === 0) { @@ -53,6 +52,8 @@ // } // } + const style = graphicsData.lineStyle; + // get first and last point.. figure out the middle! const firstPoint = new Point(points[0], points[1]); let lastPoint = new Point(points[points.length - 2], points[points.length - 1]); @@ -75,22 +76,15 @@ points.push(midPointX, midPointY); } - const verts = webGLData.points; - const indices = webGLData.indices; + const verts = graphicsGeometry.points; const length = points.length / 2; let indexCount = points.length; - let indexStart = verts.length / 6; + let indexStart = verts.length / 2; // DRAW the Line - const width = graphicsData.lineWidth / 2; + const width = style.width / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - let p1x = points[0]; let p1y = points[1]; let p2x = points[2]; @@ -112,22 +106,18 @@ perpx *= width; perpy *= width; - const ratio = graphicsData.lineAlignment;// 0.5; + const ratio = style.alignment;// 0.5; const r1 = (1 - ratio) * 2; const r2 = ratio * 2; // start verts.push( p1x - (perpx * r1), - p1y - (perpy * r1), - r, g, b, alpha - ); + p1y - (perpy * r1)); verts.push( p1x + (perpx * r2), - p1y + (perpy * r2), - r, g, b, alpha - ); + p1y + (perpy * r2)); for (let i = 1; i < length - 1; ++i) { @@ -172,15 +162,11 @@ denom += 10.1; verts.push( p2x - (perpx * r1), - p2y - (perpy * r1), - r, g, b, alpha - ); + p2y - (perpy * r1)); verts.push( p2x + (perpx * r2), - p2y + (perpy * r2), - r, g, b, alpha - ); + p2y + (perpy * r2)); continue; } @@ -201,23 +187,18 @@ perp3y *= width; verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2)); - verts.push(r, g, b, alpha); verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1)); - verts.push(r, g, b, alpha); indexCount++; } else { verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1)); - verts.push(r, g, b, alpha); verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2)); - verts.push(r, g, b, alpha); } } @@ -237,19 +218,19 @@ perpy *= width; verts.push(p2x - (perpx * r1), p2y - (perpy * r1)); - verts.push(r, g, b, alpha); verts.push(p2x + (perpx * r2), p2y + (perpy * r2)); - verts.push(r, g, b, alpha); - indices.push(indexStart); + const indices = graphicsGeometry.indices; - for (let i = 0; i < indexCount; ++i) + // indices.push(indexStart); + + for (let i = 0; i < indexCount - 2; ++i) { - indices.push(indexStart++); - } + indices.push(indexStart, indexStart + 1, indexStart + 2); - indices.push(indexStart - 1); + indexStart++; + } } /** @@ -262,22 +243,19 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -function buildNativeLine(graphicsData, webGLData) +function buildNativeLine(graphicsData, graphicsGeometry) { let i = 0; const points = graphicsData.points; if (points.length === 0) return; - const verts = webGLData.points; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; const length = points.length / 2; + let indexStart = verts.length / 2; // sort color - const color = hex2rgb(graphicsData.lineColor); - const alpha = graphicsData.lineAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; for (i = 1; i < length; i++) { @@ -288,9 +266,9 @@ const p2y = points[(i * 2) + 1]; verts.push(p1x, p1y); - verts.push(r, g, b, alpha); verts.push(p2x, p2y); - verts.push(r, g, b, alpha); + + indices.push(indexStart++, indexStart++); } } diff --git a/packages/graphics/src/utils/buildPoly.js b/packages/graphics/src/utils/buildPoly.js index 452812b..8536b70 100644 --- a/packages/graphics/src/utils/buildPoly.js +++ b/packages/graphics/src/utils/buildPoly.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a polygon to draw @@ -12,67 +11,54 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildPoly(graphicsData, webGLData, webGLDataNativeLines) -{ - graphicsData.points = graphicsData.shape.points.slice(); +export default { - let points = graphicsData.points; - - if (graphicsData.fill && points.length >= 6) + build(graphicsData) { - const holeArray = []; - // Process holes.. + graphicsData.points = graphicsData.shape.points.slice(); + }, + + triangulate(graphicsData, graphicsGeometry) + { + let points = graphicsData.points; const holes = graphicsData.holes; + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; - for (let i = 0; i < holes.length; i++) + if (points.length >= 6) { - const hole = holes[i]; + const holeArray = []; + // Process holes.. - holeArray.push(points.length / 2); + for (let i = 0; i < holes.length; i++) + { + const hole = holes[i]; - points = points.concat(hole.points); + holeArray.push(points.length / 2); + points = points.concat(hole.points); + } + + // sort color + const triangles = earcut(points, holeArray, 2); + + if (!triangles) + { + return; + } + + const vertPos = verts.length / 2; + + for (let i = 0; i < triangles.length; i += 3) + { + indices.push(triangles[i] + vertPos); + indices.push(triangles[i + 1] + vertPos); + indices.push(triangles[i + 2] + vertPos); + } + + for (let i = 0; i < points.length; i++) + { + verts.push(points[i]); + } } - - // get first and last point.. figure out the middle! - const verts = webGLData.points; - const indices = webGLData.indices; - - const length = points.length / 2; - - // sort color - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; - - const triangles = earcut(points, holeArray, 2); - - if (!triangles) - { - return; - } - - const vertPos = verts.length / 6; - - for (let i = 0; i < triangles.length; i += 3) - { - indices.push(triangles[i] + vertPos); - indices.push(triangles[i] + vertPos); - indices.push(triangles[i + 1] + vertPos); - indices.push(triangles[i + 2] + vertPos); - indices.push(triangles[i + 2] + vertPos); - } - - for (let i = 0; i < length; i++) - { - verts.push(points[i * 2], points[(i * 2) + 1], - r, g, b, alpha); - } - } - - if (graphicsData.lineWidth > 0) - { - buildLine(graphicsData, webGLData, webGLDataNativeLines); - } -} + }, +}; diff --git a/packages/graphics/src/utils/buildRectangle.js b/packages/graphics/src/utils/buildRectangle.js index 79d165f..c64c9e2 100644 --- a/packages/graphics/src/utils/buildRectangle.js +++ b/packages/graphics/src/utils/buildRectangle.js @@ -1,6 +1,3 @@ -import buildLine from './buildLine'; -import { hex2rgb } from '@pixi/utils'; - /** * Builds a rectangle to draw * @@ -12,60 +9,42 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - // --- // - // need to convert points to a nice regular data - // - const rectData = graphicsData.shape; - const x = rectData.x; - const y = rectData.y; - const width = rectData.width; - const height = rectData.height; +export default { - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + // --- // + // need to convert points to a nice regular data + // + const rectData = graphicsData.shape; + const x = rectData.x; + const y = rectData.y; + const width = rectData.width; + const height = rectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const points = graphicsData.points; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vertPos = verts.length / 6; - - // start - verts.push(x, y); - verts.push(r, g, b, alpha); - - verts.push(x + width, y); - verts.push(r, g, b, alpha); - - verts.push(x, y + height); - verts.push(r, g, b, alpha); - - verts.push(x + width, y + height); - verts.push(r, g, b, alpha); - - // insert 2 dead triangles.. - indices.push(vertPos, vertPos, vertPos + 1, vertPos + 2, vertPos + 3, vertPos + 3); - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = [x, y, + points.push(x, y, x + width, y, x + width, y + height, - x, y + height, - x, y]; + x, y + height); + }, - buildLine(graphicsData, webGLData, webGLDataNativeLines); + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + const verts = graphicsGeometry.points; - graphicsData.points = tempPoints; - } -} + const vertPos = verts.length / 2; + + verts.push(points[0], points[1], + points[2], points[3], + points[6], points[7], + points[4], points[5]); + + graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2, + vertPos + 1, vertPos + 2, vertPos + 3); + }, +}; diff --git a/packages/graphics/src/utils/buildRoundedRectangle.js b/packages/graphics/src/utils/buildRoundedRectangle.js index 6bc0059..d49a81e 100644 --- a/packages/graphics/src/utils/buildRoundedRectangle.js +++ b/packages/graphics/src/utils/buildRoundedRectangle.js @@ -1,5 +1,4 @@ -import buildLine from './buildLine'; -import { hex2rgb, earcut } from '@pixi/utils'; +import { earcut } from '@pixi/utils'; /** * Builds a rounded rectangle to draw @@ -12,69 +11,57 @@ * @param {object} webGLData - an object containing all the webGL-specific information to create this shape * @param {object} webGLDataNativeLines - an object containing all the webGL-specific information to create nativeLines */ -export default function buildRoundedRectangle(graphicsData, webGLData, webGLDataNativeLines) -{ - const rrectData = graphicsData.shape; - const x = rrectData.x; - const y = rrectData.y; - const width = rrectData.width; - const height = rrectData.height; +export default { - const radius = rrectData.radius; - - const recPoints = []; - - recPoints.push(x, y + radius); - quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, recPoints); - quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, recPoints); - quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, recPoints); - quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, recPoints); - - // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. - // TODO - fix this properly, this is not very elegant.. but it works for now. - - if (graphicsData.fill) + build(graphicsData) { - const color = hex2rgb(graphicsData.fillColor); - const alpha = graphicsData.fillAlpha; + const rrectData = graphicsData.shape; + const points = graphicsData.points; + const x = rrectData.x; + const y = rrectData.y; + const width = rrectData.width; + const height = rrectData.height; - const r = color[0] * alpha; - const g = color[1] * alpha; - const b = color[2] * alpha; + const radius = rrectData.radius; - const verts = webGLData.points; - const indices = webGLData.indices; + points.length = 0; - const vecPos = verts.length / 6; + points.push(x, y + radius); + quadraticBezierCurve(x, y + height - radius, x, y + height, x + radius, y + height, points); + quadraticBezierCurve(x + width - radius, y + height, x + width, y + height, x + width, y + height - radius, points); + quadraticBezierCurve(x + width, y + radius, x + width, y, x + width - radius, y, points); + quadraticBezierCurve(x + radius, y, x, y, x, y + radius + 0.0000000001, points); - const triangles = earcut(recPoints, null, 2); + // this tiny number deals with the issue that occurs when points overlap and earcut fails to triangulate the item. + // TODO - fix this properly, this is not very elegant.. but it works for now. + }, + + triangulate(graphicsData, graphicsGeometry) + { + const points = graphicsData.points; + + const verts = graphicsGeometry.points; + const indices = graphicsGeometry.indices; + + const vecPos = verts.length / 2; + + const triangles = earcut(points, null, 2); for (let i = 0, j = triangles.length; i < j; i += 3) { indices.push(triangles[i] + vecPos); - indices.push(triangles[i] + vecPos); + // indices.push(triangles[i] + vecPos); indices.push(triangles[i + 1] + vecPos); - indices.push(triangles[i + 2] + vecPos); + // indices.push(triangles[i + 2] + vecPos); indices.push(triangles[i + 2] + vecPos); } - for (let i = 0, j = recPoints.length; i < j; i++) + for (let i = 0, j = points.length; i < j; i++) { - verts.push(recPoints[i], recPoints[++i], r, g, b, alpha); + verts.push(points[i], points[++i]); } - } - - if (graphicsData.lineWidth) - { - const tempPoints = graphicsData.points; - - graphicsData.points = recPoints; - - buildLine(graphicsData, webGLData, webGLDataNativeLines); - - graphicsData.points = tempPoints; - } -} + }, +}; /** * Calculate a single point for a quadratic bezier curve. diff --git a/packages/graphics/test/index.js b/packages/graphics/test/index.js index b746532..4330b06 100644 --- a/packages/graphics/test/index.js +++ b/packages/graphics/test/index.js @@ -1,19 +1,17 @@ // const MockPointer = require('../interaction/MockPointer'); -const { Graphics, GraphicsRenderer } = require('../'); +const { Renderer, BatchRenderer } = require('@pixi/core'); +const { Graphics } = require('../'); const { BLEND_MODES } = require('@pixi/constants'); const { Point } = require('@pixi/math'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); -const { Renderer } = require('@pixi/core'); -const { SpriteRenderer } = require('@pixi/sprite'); + +Renderer.registerPlugin('batch', BatchRenderer); skipHello(); -Renderer.registerPlugin('graphics', GraphicsRenderer); -Renderer.registerPlugin('sprite', SpriteRenderer); - function withGL(fn) { - return isWebGLSupported() ? fn : undefined; + return isWebGLSupported() ? (fn || true) : undefined; } describe('PIXI.Graphics', function () @@ -24,9 +22,10 @@ { const graphics = new Graphics(); - expect(graphics.fillAlpha).to.be.equals(1); - expect(graphics.lineWidth).to.be.equals(0); - expect(graphics.lineColor).to.be.equals(0); + expect(graphics.fill.color).to.be.equals(0xFFFFFF); + expect(graphics.fill.alpha).to.be.equals(1); + expect(graphics.line.width).to.be.equals(0); + expect(graphics.line.color).to.be.equals(0); expect(graphics.tint).to.be.equals(0xFFFFFF); expect(graphics.blendMode).to.be.equals(BLEND_MODES.NORMAL); }); @@ -38,8 +37,8 @@ { const graphics = new Graphics(); - graphics.moveTo(0, 0); graphics.lineStyle(1); + graphics.moveTo(0, 0); graphics.lineTo(0, 10); expect(graphics.width).to.be.below(1.00001); @@ -128,7 +127,7 @@ graphics.lineTo(10, 0); graphics.lineTo(10, 0); - expect(graphics.currentPath.shape.points).to.deep.equal([0, 0, 10, 0]); + expect(graphics.currentPath.points).to.deep.equal([0, 0, 10, 0]); }); }); @@ -177,18 +176,47 @@ .lineTo(10, 0) .lineTo(10, 10) .lineTo(0, 10) - // draw hole + .beginHole() .moveTo(2, 2) .lineTo(8, 2) .lineTo(8, 8) .lineTo(2, 8) - .addHole(); + .endHole(); expect(graphics.containsPoint(point1)).to.be.true; expect(graphics.containsPoint(point2)).to.be.false; }); }); + describe('chaining', function () + { + it('should chain draw commands', function () + { + // complex drawing #1: draw triangle, rounder rect and an arc (issue #3433) + const graphics = new Graphics().beginFill(0xFF3300) + .lineStyle(4, 0xffd900, 1) + .moveTo(50, 50) + .lineTo(250, 50) + .endFill() + .drawRoundedRect(150, 450, 300, 100, 15) + .beginHole() + .endHole() + .quadraticCurveTo(1, 1, 1, 1) + .bezierCurveTo(1, 1, 1, 1) + .arcTo(1, 1, 1, 1, 1) + .arc(1, 1, 1, 1, 1, false) + .drawRect(1, 1, 1, 1) + .drawRoundedRect(1, 1, 1, 1, 0.1) + .drawCircle(1, 1, 20) + .drawEllipse(1, 1, 1, 1) + .drawPolygon([1, 1, 1, 1, 1, 1]) + .drawStar(1, 1, 1, 1, 1, 1) + .clear(); + + expect(graphics).to.be.not.null; + }); + }); + describe('arc', function () { it('should draw an arc', function () @@ -257,7 +285,7 @@ it('should only call updateLocalBounds once', function () { const graphics = new Graphics(); - const spy = sinon.spy(graphics, 'updateLocalBounds'); + const spy = sinon.spy(graphics.geometry, 'calculateBounds'); graphics._calculateBounds(); @@ -269,51 +297,9 @@ }); }); - describe('fastRect', function () - { - it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () - { - const renderer = new Renderer(200, 200, {}); - - try - { - const graphics = new Graphics(); - - graphics.beginFill(0x102030, 0.6); - graphics.drawRect(2, 3, 100, 100); - graphics.endFill(); - graphics.tint = 0x101010; - graphics.blendMode = 2; - graphics.alpha = 0.3; - - renderer.render(graphics); - - expect(graphics.isFastRect()).to.be.true; - - const sprite = graphics._spriteRect; - - expect(sprite).to.not.be.equals(null); - expect(sprite.worldAlpha).to.equals(0.18); - expect(sprite.blendMode).to.equals(2); - expect(sprite.tint).to.equals(0x010203); - - const bounds = sprite.getBounds(); - - expect(bounds.x).to.equals(2); - expect(bounds.y).to.equals(3); - expect(bounds.width).to.equals(100); - expect(bounds.height).to.equals(100); - } - finally - { - renderer.destroy(); - } - })); - }); - describe('drawCircle', function () { - it('should have no gaps in line border', withGL(function () + it.skip('should have no gaps in line border', withGL(function () { const renderer = new Renderer(200, 200, {}); @@ -326,7 +312,7 @@ renderer.render(graphics); - const points = graphics._webGL[0].data[0].points; + const points = graphics.geometry.graphicsData[0].points;// ._webGL[0].data[0].points; const pointSize = 6; // Position Vec2 + Color/Alpha Vec4 const firstX = points[0]; const firstY = points[1]; @@ -348,4 +334,35 @@ } })); }); + + describe('drawCircle', function () + { + it('should have no gaps in line border', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.lineStyle(15, 0x8FC7E6); + graphics.drawCircle(100, 100, 30); + renderer.render(graphics); + const points = graphics.geometry.graphicsData[0].points; + + const firstX = points[0]; + const firstY = points[1]; + + const lastX = points[points.length - 2]; + const lastY = points[points.length - 1]; + + expect(firstX).to.equals(lastX); + expect(firstY).to.equals(lastY); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/packages/mesh-extras/src/Mesh2d.js b/packages/mesh-extras/src/Mesh2d.js deleted file mode 100644 index d113a5e..0000000 --- a/packages/mesh-extras/src/Mesh2d.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Mesh } from '@pixi/mesh'; -import { Geometry, Program, Shader, Texture, TextureMatrix } from '@pixi/core'; -import { Matrix } from '@pixi/math'; -import { BLEND_MODES } from '@pixi/constants'; -import { hex2rgb, premultiplyRgba } from '@pixi/utils'; -import vertex from './mesh.vert'; -import fragment from './mesh.frag'; - -let meshProgram; - -/** - * Base mesh class - * @class - * @extends PIXI.Container - * @memberof PIXI - */ -export default class Mesh2d extends Mesh -{ - /** - * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use - * @param {Float32Array} [vertices] - if you want to specify the vertices - * @param {Float32Array} [uvs] - if you want to specify the uvs - * @param {Uint16Array} [indices] - if you want to specify the indices - * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts - */ - constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) - { - const geometry = new Geometry(); - - if (!meshProgram) - { - meshProgram = new Program(vertex, fragment); - } - - geometry.addAttribute('aVertexPosition', vertices) - .addAttribute('aTextureCoord', uvs) - .addIndex(indices); - - geometry.getAttribute('aVertexPosition').static = false; - - const uniforms = { - uSampler: texture, - uTextureMatrix: Matrix.IDENTITY, - alpha: 1, - uColor: new Float32Array([1, 1, 1, 1]), - }; - - super(geometry, new Shader(meshProgram, uniforms), null, drawMode); - - /** - * The Uvs of the Mesh - * - * @member {Float32Array} - */ - this.uvs = geometry.getAttribute('aTextureCoord').data; - - /** - * An array of vertices - * - * @member {Float32Array} - */ - this.vertices = geometry.getAttribute('aVertexPosition').data; - - this.uniforms = uniforms; - - /** - * The texture of the Mesh - * - * @member {PIXI.Texture} - * @default PIXI.Texture.EMPTY - * @private - */ - this.texture = texture; - - /** - * The tint applied to the mesh. This is a [r,g,b] value. A value of [1,1,1] will remove any - * tint effect. - * - * @member {number} - * @private - */ - this._tintRGB = new Float32Array([1, 1, 1]); - - // Set default tint - this.tint = 0xFFFFFF; - - this.blendMode = BLEND_MODES.NORMAL; - - /** - * TextureMatrix instance for this Mesh, used to track Texture changes - * - * @member {PIXI.TextureMatrix} - * @readonly - */ - this.uvMatrix = new TextureMatrix(this._texture); - - /** - * whether or not upload uvMatrix to shader - * if its false, then uvs should be pre-multiplied - * if you change it for generated mesh, please call 'refresh(true)' - * @member {boolean} - * @default false - */ - this.uploadUvMatrix = false; - - /** - * Uploads vertices buffer every frame, like in PixiJS V3 and V4. - * - * @member {boolean} - * @default true - */ - this.autoUpdate = true; - } - - /** - * The tint applied to the Rope. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. - * - * @member {number} - * @memberof PIXI.Sprite# - * @default 0xFFFFFF - */ - get tint() - { - return this._tint; - } - - /** - * Sets the tint of the rope. - * - * @param {number} value - The value to set to. - */ - set tint(value) - { - this._tint = value; - - hex2rgb(this._tint, this._tintRGB); - } - - /** - * The blend mode to be applied to the sprite. Set to `PIXI.BLEND_MODES.NORMAL` to remove any blend mode. - * - * @member {number} - * @default PIXI.BLEND_MODES.NORMAL - * @see PIXI.BLEND_MODES - */ - get blendMode() - { - return this.state.blendMode; - } - - set blendMode(value) // eslint-disable-line require-jsdoc - { - this.state.blendMode = value; - } - - /** - * The texture that the mesh uses. - * - * @member {PIXI.Texture} - */ - get texture() - { - return this._texture; - } - - set texture(value) // eslint-disable-line require-jsdoc - { - if (this._texture === value) - { - return; - } - - this._texture = value; - this.uniforms.uSampler = this.texture; - - if (value) - { - // wait for the texture to load - if (value.baseTexture.valid) - { - this._onTextureUpdate(); - } - else - { - value.once('update', this._onTextureUpdate, this); - } - } - } - - _render(renderer) - { - const baseTex = this._texture.baseTexture; - - premultiplyRgba(this._tintRGB, this.worldAlpha, this.uniforms.uColor, baseTex.premultiplyAlpha); - super._render(renderer); - } - /** - * When the texture is updated, this event will fire to update the scale and frame - * - * @private - */ - _onTextureUpdate() - { - // constructor texture update stop - if (!this.uvMatrix) - { - return; - } - this.uvMatrix.texture = this._texture; - this.refresh(); - } - - /** - * multiplies uvs only if uploadUvMatrix is false - * call it after you change uvs manually - * make sure that texture is valid - */ - multiplyUvs() - { - if (!this.uploadUvMatrix) - { - this.uvMatrix.multiplyUvs(this.uvs); - } - } - - /** - * Refreshes uvs for generated meshes (rope, plane) - * sometimes refreshes vertices too - * - * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case - */ - refresh(forceUpdate) - { - if (this.uvMatrix.update(forceUpdate)) - { - this._refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.geometry.buffers[0].update(); - } - this.containerUpdateTransform(); - } - - /** - * re-calculates mesh coords - * @private - */ - _refresh() - { - /* empty */ - } -} diff --git a/packages/mesh-extras/src/NineSlicePlane.js b/packages/mesh-extras/src/NineSlicePlane.js index 0bae92c..e5a4086 100644 --- a/packages/mesh-extras/src/NineSlicePlane.js +++ b/packages/mesh-extras/src/NineSlicePlane.js @@ -1,4 +1,4 @@ -import Plane from './Plane'; +import SimplePlane from './SimplePlane'; const DEFAULT_BORDER_SIZE = 10; @@ -33,7 +33,7 @@ * @memberof PIXI * */ -export default class NineSlicePlane extends Plane +export default class NineSlicePlane extends SimplePlane { /** * @param {PIXI.Texture} texture - The texture to use on the NineSlicePlane. @@ -102,8 +102,21 @@ * @override */ this._bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + } - this.refresh(true); + textureUpdated() + { + this._refresh(); + } + + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; } /** @@ -239,10 +252,9 @@ */ _refresh() { - super._refresh(); + const texture = this.texture; - const uvs = this.uvs; - const texture = this._texture; + const uvs = this.geometry.buffers[1].data; this._origWidth = texture.orig.width; this._origHeight = texture.orig.height; @@ -263,8 +275,7 @@ this.updateHorizontalVertices(); this.updateVerticalVertices(); + this.geometry.buffers[0].update(); this.geometry.buffers[1].update(); - - this.multiplyUvs(); } } diff --git a/packages/mesh-extras/src/Plane.js b/packages/mesh-extras/src/Plane.js deleted file mode 100644 index f731427..0000000 --- a/packages/mesh-extras/src/Plane.js +++ /dev/null @@ -1,102 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The Plane allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Plane extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the Plane. - * @param {number} [verticesX=10] - The number of vertices in the x-axis - * @param {number} [verticesY=10] - The number of vertices in the y-axis - * @param {object} [options] - an options object - * @param {number} [options.meshWidth=0] - The default mesh width - * @param {number} [options.meshHeight=0] - The default mesh height - */ - constructor(texture, verticesX, verticesY, options = {}) - { - super(texture, new Float32Array(1), new Float32Array(1), new Uint16Array(1), 4); - - this.verticesX = verticesX || 10; - this.verticesY = verticesY || 10; - - this.meshWidth = options.meshWidth || 0; - this.meshHeight = options.meshHeight || 0; - - this.refresh(); - } - - /** - * Refreshes plane coordinates - * @private - */ - _refresh() - { - const texture = this._texture; - const total = this.verticesX * this.verticesY; - const verts = []; - const uvs = []; - const indices = []; - - const segmentsX = this.verticesX - 1; - const segmentsY = this.verticesY - 1; - - const sizeX = (this.meshWidth || texture.width) / segmentsX; - const sizeY = (this.meshHeight || texture.height) / segmentsY; - - for (let i = 0; i < total; i++) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); - - verts.push(x * sizeX, y * sizeY); - - uvs.push(x / segmentsX, y / segmentsY); - } - - // cons - - const totalSub = segmentsX * segmentsY; - - for (let i = 0; i < totalSub; i++) - { - const xpos = i % segmentsX; - const ypos = (i / segmentsX) | 0; - - const value = (ypos * this.verticesX) + xpos; - const value2 = (ypos * this.verticesX) + xpos + 1; - const value3 = ((ypos + 1) * this.verticesX) + xpos; - const value4 = ((ypos + 1) * this.verticesX) + xpos + 1; - - indices.push(value, value2, value3); - indices.push(value2, value4, value3); - } - - this.vertices = new Float32Array(verts); - this.uvs = new Float32Array(uvs); - this.indices = new Uint16Array(indices); - - this.geometry.buffers[0].data = this.vertices; - this.geometry.buffers[1].data = this.uvs; - this.geometry.indexBuffer.data = this.indices; - - // ensure that the changes are uploaded - this.geometry.buffers[0].update(); - this.geometry.buffers[1].update(); - this.geometry.indexBuffer.update(); - - this.multiplyUvs(); - } -} diff --git a/packages/mesh-extras/src/Rope.js b/packages/mesh-extras/src/Rope.js deleted file mode 100644 index 9cfde38..0000000 --- a/packages/mesh-extras/src/Rope.js +++ /dev/null @@ -1,182 +0,0 @@ -import Mesh2d from './Mesh2d'; - -/** - * The rope allows you to draw a texture across several points and them manipulate these points - * - *```js - * for (let i = 0; i < 20; i++) { - * points.push(new PIXI.Point(i * 50, 0)); - * }; - * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); - * ``` - * - * @class - * @extends PIXI.Mesh - * @memberof PIXI - * - */ -export default class Rope extends Mesh2d -{ - /** - * @param {PIXI.Texture} texture - The texture to use on the rope. - * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. - */ - constructor(texture, points) - { - super(texture, new Float32Array(points.length * 4), - new Float32Array(points.length * 4), - new Uint16Array(points.length * 2), - 5); - - /* - * @member {PIXI.Point[]} An array of points that determine the rope - */ - this.points = points; - this.refresh(); - } - /** - * Refreshes Rope indices and uvs - * @private - */ - _refresh() - { - const points = this.points; - - if (!points) return; - - const vertexBuffer = this.geometry.getAttribute('aVertexPosition'); - const uvBuffer = this.geometry.getAttribute('aTextureCoord'); - const indexBuffer = this.geometry.getIndex(); - - // if too little points, or texture hasn't got UVs set yet just move on. - if (points.length < 1 || !this.texture._uvs) - { - return; - } - - // if the number of points has changed we will need to recreate the arraybuffers - if (vertexBuffer.data.length / 4 !== points.length) - { - vertexBuffer.data = new Float32Array(points.length * 4); - uvBuffer.data = new Float32Array(points.length * 4); - indexBuffer.data = new Uint16Array(points.length * 2); - } - - const uvs = uvBuffer.data; - const indices = indexBuffer.data; - - uvs[0] = 0; - uvs[1] = 0; - uvs[2] = 0; - uvs[3] = 1; - - indices[0] = 0; - indices[1] = 1; - - const total = points.length; - - for (let i = 1; i < total; i++) - { - // time to do some smart drawing! - let index = i * 4; - const amount = i / (total - 1); - - uvs[index] = amount; - uvs[index + 1] = 0; - - uvs[index + 2] = amount; - uvs[index + 3] = 1; - - index = i * 2; - indices[index] = index; - indices[index + 1] = index + 1; - } - - // ensure that the changes are uploaded - uvBuffer.update(); - indexBuffer.update(); - - this.multiplyUvs(); - this.refreshVertices(); - } - - /** - * refreshes vertices of Rope mesh - */ - refreshVertices() - { - const points = this.points; - - if (points.length < 1) - { - return; - } - - let lastPoint = points[0]; - let nextPoint; - let perpX = 0; - let perpY = 0; - - // this.count -= 0.2; - - const vertices = this.vertices; - const total = points.length; - - for (let i = 0; i < total; i++) - { - const point = points[i]; - const index = i * 4; - - if (i < points.length - 1) - { - nextPoint = points[i + 1]; - } - else - { - nextPoint = point; - } - - perpY = -(nextPoint.x - lastPoint.x); - perpX = nextPoint.y - lastPoint.y; - - let ratio = (1 - (i / (total - 1))) * 10; - - if (ratio > 1) - { - ratio = 1; - } - - const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); - const num = this._texture.height / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; - - perpX /= perpLength; - perpY /= perpLength; - - perpX *= num; - perpY *= num; - - vertices[index] = point.x + perpX; - vertices[index + 1] = point.y + perpY; - vertices[index + 2] = point.x - perpX; - vertices[index + 3] = point.y - perpY; - - lastPoint = point; - } - - this.geometry.buffers[0].update(); - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() - { - if (this.autoUpdate) - { - this.refreshVertices(); - } - this.containerUpdateTransform(); - } -} diff --git a/packages/mesh-extras/src/SimpleMesh.js b/packages/mesh-extras/src/SimpleMesh.js new file mode 100644 index 0000000..283ddef --- /dev/null +++ b/packages/mesh-extras/src/SimpleMesh.js @@ -0,0 +1,56 @@ +import { Mesh, MeshGeometry, MeshMaterial } from '@pixi/mesh'; +import { Texture } from '@pixi/core'; + +/** + * Simple Mesh class mimics mesh in PixiJS v4, provides + * easy-to-use constructor arguments. For more robust + * customization, use {@link PIXI.Mesh}. + * @class + * @extends PIXI.Mesh + * @memberof PIXI + */ +export default class SimpleMesh extends Mesh +{ + /** + * @param {PIXI.Texture} [texture=Texture.EMPTY] - The texture to use + * @param {Float32Array} [vertices] - if you want to specify the vertices + * @param {Float32Array} [uvs] - if you want to specify the uvs + * @param {Uint16Array} [indices] - if you want to specify the indices + * @param {number} [drawMode] - the drawMode, can be any of the Mesh.DRAW_MODES consts + */ + constructor(texture = Texture.EMPTY, vertices, uvs, indices, drawMode) + { + const geometry = new MeshGeometry(vertices, uvs, indices); + + geometry.getAttribute('aVertexPosition').static = false; + + const meshMaterial = new MeshMaterial(texture); + + super(geometry, meshMaterial, null, drawMode); + + this.autoUpdate = true; + } + + /** + * Collection of vertices data. + * @member {Float32Array} + */ + get vertices() + { + return this.geometry.getAttribute('aVertexPosition').data; + } + set vertices(value) + { + this.geometry.getAttribute('aVertexPosition').data = value; + } + + _render(renderer) + { + if (this.autoUpdate) + { + this.geometry.getAttribute('aVertexPosition').update(); + } + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/SimplePlane.js b/packages/mesh-extras/src/SimplePlane.js new file mode 100644 index 0000000..7b572ce --- /dev/null +++ b/packages/mesh-extras/src/SimplePlane.js @@ -0,0 +1,47 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import PlaneGeometry from './geometry/PlaneGeometry'; + +/** + * The Plane allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let Plane = new PIXI.Plane(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimplePlane extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the Plane. + * @param {number} verticesX - The number of vertices in the x-axis + * @param {number} verticesY - The number of vertices in the y-axis + */ + constructor(texture, verticesX, verticesY) + { + const planeGeometry = new PlaneGeometry(texture.width, texture.height, verticesX, verticesY); + const meshMaterial = new MeshMaterial(texture); + + super(planeGeometry, meshMaterial); + + // wait for the texture to load + if (!texture.baseTexture.hasLoaded) + { + texture.once('update', this.textureUpdated, this); + } + } + + textureUpdated() + { + this.geometry.width = this.shader.texture.width; + this.geometry.height = this.shader.texture.height; + + this.geometry.build(); + } +} diff --git a/packages/mesh-extras/src/SimpleRope.js b/packages/mesh-extras/src/SimpleRope.js new file mode 100644 index 0000000..ba8a4cf --- /dev/null +++ b/packages/mesh-extras/src/SimpleRope.js @@ -0,0 +1,39 @@ +import { Mesh, MeshMaterial } from '@pixi/mesh'; +import RopeGeometry from './geometry/RopeGeometry'; + +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.Mesh + * @memberof PIXI + * + */ +export default class SimpleRope extends Mesh +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(texture, points) + { + const ropeGeometry = new RopeGeometry(texture.height, points); + const meshMaterial = new MeshMaterial(texture); + + super(ropeGeometry, meshMaterial); + } + + _render(renderer) + { + this.geometry.width = this.shader.texture.height; + + super._render(renderer); + } +} diff --git a/packages/mesh-extras/src/geometry/PlaneGeometry.js b/packages/mesh-extras/src/geometry/PlaneGeometry.js new file mode 100644 index 0000000..81690e0 --- /dev/null +++ b/packages/mesh-extras/src/geometry/PlaneGeometry.js @@ -0,0 +1,70 @@ +import { MeshGeometry } from '@pixi/mesh'; + +export default class PlaneGeometry extends MeshGeometry +{ + constructor(width = 100, height = 100, segWidth = 10, segHeight = 10) + { + super(); + + this.segWidth = segWidth; + this.segHeight = segHeight; + + this.width = width; + this.height = height; + + // console.log('>>>>>', segWidth, segHeight); + this.build(); + } + + /** + * Refreshes plane coordinates + * @private + */ + build() + { + const total = this.segWidth * this.segHeight; + const verts = []; + const uvs = []; + const indices = []; + + const segmentsX = this.segWidth - 1; + const segmentsY = this.segHeight - 1; + + const sizeX = (this.width) / segmentsX; + const sizeY = (this.height) / segmentsY; + + for (let i = 0; i < total; i++) + { + const x = (i % this.segWidth); + const y = ((i / this.segWidth) | 0); + + verts.push(x * sizeX, y * sizeY); + uvs.push(x / segmentsX, y / segmentsY); + } + + const totalSub = segmentsX * segmentsY; + + for (let i = 0; i < totalSub; i++) + { + const xpos = i % segmentsX; + const ypos = (i / segmentsX) | 0; + + const value = (ypos * this.segWidth) + xpos; + const value2 = (ypos * this.segWidth) + xpos + 1; + const value3 = ((ypos + 1) * this.segWidth) + xpos; + const value4 = ((ypos + 1) * this.segWidth) + xpos + 1; + + indices.push(value, value2, value3, + value2, value4, value3); + } + + this.buffers[0].data = new Float32Array(verts); + this.buffers[1].data = new Float32Array(uvs); + this.indexBuffer.data = new Uint16Array(indices); + + // ensure that the changes are uploaded + this.buffers[0].update(); + this.buffers[1].update(); + this.indexBuffer.update(); + } +} diff --git a/packages/mesh-extras/src/geometry/RopeGeometry.js b/packages/mesh-extras/src/geometry/RopeGeometry.js new file mode 100644 index 0000000..4695296 --- /dev/null +++ b/packages/mesh-extras/src/geometry/RopeGeometry.js @@ -0,0 +1,184 @@ +import { MeshGeometry } from '@pixi/mesh'; +/** + * The rope allows you to draw a texture across several points and them manipulate these points + * + *```js + * for (let i = 0; i < 20; i++) { + * points.push(new PIXI.Point(i * 50, 0)); + * }; + * let rope = new PIXI.Rope(PIXI.Texture.from("snake.png"), points); + * ``` + * + * @class + * @extends PIXI.MeshGeometry + * @memberof PIXI + * + */ +export default class RopeGeometry extends MeshGeometry +{ + /** + * @param {PIXI.Texture} texture - The texture to use on the rope. + * @param {PIXI.Point[]} points - An array of {@link PIXI.Point} objects to construct this rope. + */ + constructor(width = 200, points) + { + super(new Float32Array(points.length * 4), + new Float32Array(points.length * 4), + new Uint16Array((points.length - 1) * 6)); + + /* + * @member {PIXI.Point[]} An array of points that determine the rope + */ + this.points = points; + + this.width = width; + + this.build(); + } + /** + * Refreshes Rope indices and uvs + * @private + */ + build() + { + const points = this.points; + + if (!points) return; + + const vertexBuffer = this.getAttribute('aVertexPosition'); + const uvBuffer = this.getAttribute('aTextureCoord'); + const indexBuffer = this.getIndex(); + + // if too little points, or texture hasn't got UVs set yet just move on. + if (points.length < 1) + { + return; + } + + // if the number of points has changed we will need to recreate the arraybuffers + if (vertexBuffer.data.length / 4 !== points.length) + { + vertexBuffer.data = new Float32Array(points.length * 4); + uvBuffer.data = new Float32Array(points.length * 4); + indexBuffer.data = new Uint16Array((points.length - 1) * 6); + } + + const uvs = uvBuffer.data; + const indices = indexBuffer.data; + + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; + + // indices[0] = 0; + // indices[1] = 1; + + const total = points.length; // - 1; + + for (let i = 0; i < total; i++) + { + // time to do some smart drawing! + const index = i * 4; + const amount = i / (total - 1); + + uvs[index] = amount; + uvs[index + 1] = 0; + + uvs[index + 2] = amount; + uvs[index + 3] = 1; + } + + let indexCount = 0; + + for (let i = 0; i < total - 1; i++) + { + const index = i * 2; + + indices[indexCount++] = index; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 2; + + indices[indexCount++] = index + 2; + indices[indexCount++] = index + 1; + indices[indexCount++] = index + 3; + } + + // ensure that the changes are uploaded + uvBuffer.update(); + indexBuffer.update(); + + this.updateVertices(); + } + + /** + * refreshes vertices of Rope mesh + */ + updateVertices() + { + const points = this.points; + + if (points.length < 1) + { + return; + } + + let lastPoint = points[0]; + let nextPoint; + let perpX = 0; + let perpY = 0; + + // this.count -= 0.2; + + const vertices = this.buffers[0].data; + const total = points.length; + + for (let i = 0; i < total; i++) + { + const point = points[i]; + const index = i * 4; + + if (i < points.length - 1) + { + nextPoint = points[i + 1]; + } + else + { + nextPoint = point; + } + + perpY = -(nextPoint.x - lastPoint.x); + perpX = nextPoint.y - lastPoint.y; + + let ratio = (1 - (i / (total - 1))) * 10; + + if (ratio > 1) + { + ratio = 1; + } + + const perpLength = Math.sqrt((perpX * perpX) + (perpY * perpY)); + const num = this.width / 2; // (20 + Math.abs(Math.sin((i + this.count) * 0.3) * 50) )* ratio; + + perpX /= perpLength; + perpY /= perpLength; + + perpX *= num; + perpY *= num; + + vertices[index] = point.x + perpX; + vertices[index + 1] = point.y + perpY; + vertices[index + 2] = point.x - perpX; + vertices[index + 3] = point.y - perpY; + + lastPoint = point; + } + + this.buffers[0].update(); + } + + update() + { + this.updateVertices(); + } +} diff --git a/packages/mesh-extras/src/index.js b/packages/mesh-extras/src/index.js index decf454..adc467f 100644 --- a/packages/mesh-extras/src/index.js +++ b/packages/mesh-extras/src/index.js @@ -1,4 +1,6 @@ -export { default as Mesh2d } from './Mesh2d'; -export { default as Plane } from './Plane'; +export { default as PlaneGeometry } from './geometry/PlaneGeometry'; +export { default as RopeGeometry } from './geometry/RopeGeometry'; +export { default as SimpleRope } from './SimpleRope'; +export { default as SimplePlane } from './SimplePlane'; +export { default as SimpleMesh } from './SimpleMesh'; export { default as NineSlicePlane } from './NineSlicePlane'; -export { default as Rope } from './Rope'; diff --git a/packages/mesh-extras/src/mesh.frag b/packages/mesh-extras/src/mesh.frag deleted file mode 100644 index 6096983..0000000 --- a/packages/mesh-extras/src/mesh.frag +++ /dev/null @@ -1,9 +0,0 @@ -varying vec2 vTextureCoord; -uniform vec4 uColor; - -uniform sampler2D uSampler; - -void main(void) -{ - gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; -} diff --git a/packages/mesh-extras/src/mesh.vert b/packages/mesh-extras/src/mesh.vert deleted file mode 100644 index 0526df9..0000000 --- a/packages/mesh-extras/src/mesh.vert +++ /dev/null @@ -1,15 +0,0 @@ -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; - -uniform mat3 projectionMatrix; -uniform mat3 translationMatrix; -uniform mat3 uTextureMatrix; - -varying vec2 vTextureCoord; - -void main(void) -{ - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; -} diff --git a/packages/mesh-extras/test/Plane.js b/packages/mesh-extras/test/Plane.js index f9b1c25..288bb3c 100644 --- a/packages/mesh-extras/test/Plane.js +++ b/packages/mesh-extras/test/Plane.js @@ -1,4 +1,4 @@ -const { Plane } = require('../'); +const { SimplePlane } = require('../'); const { isWebGLSupported, skipHello } = require('@pixi/utils'); const { Loader } = require('@pixi/loaders'); const { Point } = require('@pixi/math'); @@ -12,47 +12,47 @@ } // TODO: fix with webglrenderer -describe('PIXI.mesh.Plane', function () +describe('PIXI.SimplePlane', function () { - it.skip('should create a plane from an external image', withGL(function (done) + it('should create a plane from an external image', withGL(function (done) { const loader = new Loader(); loader.add('testBitmap', `file://${__dirname}/resources/bitmap-1.png`) .load(function (loader, resources) { - const plane = new Plane(resources.testBitmap.texture, 100, 100); + const plane = new SimplePlane(resources.testBitmap.texture, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); done(); }); })); - it.skip('should create a new empty textured Plane', withGL(function () + it('should create a new empty textured SimplePlane', withGL(function () { - const plane = new Plane(Texture.EMPTY, 100, 100); + const plane = new SimplePlane(Texture.EMPTY, 100, 100); - expect(plane.verticesX).to.equal(100); - expect(plane.verticesY).to.equal(100); + expect(plane.geometry.segWidth).to.equal(100); + expect(plane.geometry.segHeight).to.equal(100); })); describe('containsPoint', function () { - it.skip('should return true when point inside', withGL(function () + it('should return true when point inside', withGL(function () { const point = new Point(10, 10); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.true; })); - it.skip('should return false when point outside', withGL(function () + it('should return false when point outside', withGL(function () { const point = new Point(100, 100); const texture = new RenderTexture.create(20, 30); - const plane = new Plane(texture, 100, 100); + const plane = new SimplePlane(texture, 100, 100); expect(plane.containsPoint(point)).to.be.false; })); diff --git a/packages/mesh/package.json b/packages/mesh/package.json index ad5851e..2613f36 100644 --- a/packages/mesh/package.json +++ b/packages/mesh/package.json @@ -25,7 +25,8 @@ "@pixi/constants": "^5.0.0-alpha.3", "@pixi/core": "^5.0.0-alpha.3", "@pixi/display": "^5.0.0-alpha.3", - "@pixi/math": "^5.0.0-alpha.3" + "@pixi/math": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3" }, "devDependencies": { "floss": "^2.1.3" diff --git a/packages/mesh/src/Mesh.js b/packages/mesh/src/Mesh.js index 1f35b7a..870e3c4 100644 --- a/packages/mesh/src/Mesh.js +++ b/packages/mesh/src/Mesh.js @@ -1,21 +1,19 @@ import { State } from '@pixi/core'; -import { DRAW_MODES } from '@pixi/constants'; import { Point, Polygon } from '@pixi/math'; +import { BLEND_MODES, DRAW_MODES } from '@pixi/constants'; import { Container } from '@pixi/display'; const tempPoint = new Point(); const tempPolygon = new Polygon(); /** - * Base mesh class. + * Base mesh class * The reason for this class is to empower you to have maximum flexibility to render any kind of webGL you can think of. * This class assumes a certain level of webGL knowledge. * If you know a bit this should abstract enough away to make you life easier! * Pretty much ALL WebGL can be broken down into the following: * Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc.. * Shader - This is the shader that pixi will render the geometry with. (attributes in the shader must match the geometry!) - * Uniforms - These are the values passed to the shader when the mesh is rendered. - * As a shader can be reused across multiple objects, it made sense to allow uniforms to exist outside of the shader * State - This is the state of WebGL required to render the mesh. * Through a combination of the above elements you can render anything you want, 2D or 3D! * @@ -27,76 +25,269 @@ { /** * @param {PIXI.Geometry} geometry the geometry the mesh will use - * @param {PIXI.Shader} shader the shader the mesh will use - * @param {PIXI.State} state the state that the webGL context is required to be in to render the mesh - * @param {number} drawMode the drawMode, can be any of the PIXI.DRAW_MODES consts + * @param {PIXI.Shader|PIXI.MeshMaterial} shader the shader the mesh will use + * @param {PIXI.State} [state] the state that the webGL context is required to be in to render the mesh + * if no state is provided, uses {@link PIXI.State.for2d} to create a 2D state for PixiJS. + * @param {number} [drawMode=PIXI.DRAW_MODES.TRIANGLES] the drawMode, can be any of the PIXI.DRAW_MODES consts */ - constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES) + constructor(geometry, shader, state, drawMode = DRAW_MODES.TRIANGLES)// vertices, uvs, indices, drawMode) { super(); /** - * the geometry the mesh will use - * @type {PIXI.Geometry} + * Includes vertex positions, face indices, normals, colors, UVs, and + * custom attributes within buffers, reducing the cost of passing all + * this data to the GPU. Can be shared between multiple Mesh objects. + * @member {PIXI.Geometry} */ this.geometry = geometry; /** - * the shader the mesh will use - * @type {PIXI.Shader} + * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU. + * Can be shared between multiple Mesh objects. + * @member {PIXI.Shader|PIXI.MeshMaterial} */ this.shader = shader; /** - * the webGL state the mesh requires to render - * @type {PIXI.State} + * Represents the webGL state the Mesh required to render, excludes shader and geometry. E.g., + * blend mode, culling, depth testing, direction of rendering triangles, backface, etc. + * @member {PIXI.State} */ - this.state = state || new State(); + this.state = state || State.for2d(); /** - * The way the Mesh should be drawn, can be any of the {@link PIXI.Mesh.DRAW_MODES} consts + * The way the Mesh should be drawn, can be any of the {@link PIXI.DRAW_MODES} constants. * * @member {number} - * @see PIXI.Mesh.DRAW_MODES + * @see PIXI.DRAW_MODES */ this.drawMode = drawMode; /** - * The way uniforms that will be used by the mesh's shader. - * @member {Object} + * Typically the index of the IndexBuffer where to start drawing. + * @member {number} + * @default 0 */ - - /** - * A map of renderer IDs to webgl render data - * - * @private - * @member {object} - */ - this._glDatas = {}; - - /** - * Plugin that is responsible for rendering this element. - * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. - * - * @member {string} - * @default 'mesh' - */ - this.pluginName = 'mesh'; - this.start = 0; + + /** + * How much of the geometry to draw, by default `0` renders everything. + * @member {number} + * @default 0 + */ this.size = 0; + + /** + * thease are used as easy access for batching + * @member {Float32Array} + * @private + */ + this.uvs = null; + + /** + * thease are used as easy access for batching + * @member {Uint16Array} + * @private + */ + this.indices = null; + + /** + * this is the caching layer used by the batcher + * @member {Float32Array} + * @private + */ + this.vertexData = new Float32Array(1); + + /** + * If geometry is changed used to decide to re-transform + * the vertexData. + * @member {number} + * @private + */ + this.vertexDirty = 0; + + // Inherited from DisplayMode, set defaults + this.tint = 0xFFFFFF; + this.blendMode = BLEND_MODES.NORMAL; } /** - * Renders the object using the WebGL renderer + * Alias for {@link PIXI.Mesh#shader}. + * @member {PIXI.Shader|PIXI.MeshMaterial} + */ + set material(value) + { + this.shader = value; + } + + get material() + { + return this.shader; + } + + /** + * The blend mode to be applied to the graphic shape. Apply a value of + * `PIXI.BLEND_MODES.NORMAL` to reset the blend mode. * - * @param {PIXI.Renderer} renderer a reference to the WebGL renderer + * @member {number} + * @default PIXI.BLEND_MODES.NORMAL; + * @see PIXI.BLEND_MODES + */ + set blendMode(value) + { + this.state.blendMode = value; + } + + get blendMode() + { + return this.state.blendMode; + } + + /** + * The multiply tint applied to the Mesh. This is a hex value. A value of + * `0xFFFFFF` will remove any tint effect. + * + * @member {number} + * @default 0xFFFFFF + */ + get tint() + { + return this.shader.tint; + } + + set tint(value) + { + this.shader.tint = value; + } + + /** + * The texture that the Mesh uses. + * + * @member {PIXI.Texture} + */ + get texture() + { + return this.shader.texture; + } + + set texture(value) + { + this.shader.texture = value; + } + + /** + * Standard renderer draw. * @private */ _render(renderer) { - renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]); - renderer.plugins[this.pluginName].render(this); + // set properties for batching.. + // TODO could use a different way to grab verts? + const vertices = this.geometry.buffers[0].data; + + if (this.geometry.update && this.geometry._updateId !== renderer.tick) + { + this.geometry._updateId = renderer.tick; + this.geometry.update(); + } + + // TODO benchmark check for attribute size.. + if (this.shader.batchable && this.drawMode === DRAW_MODES.TRIANGLES && vertices.length < Mesh.BATCHABLE_SIZE * 2) + { + this._renderToBatch(renderer); + } + else + { + this._renderDefault(renderer); + } + } + + /** + * Standard non-batching way of rendering. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderDefault(renderer) + { + const shader = this.shader; + + if (shader.update) + { + shader.update(); + } + + renderer.batch.flush(); + + if (shader.program.uniformData.translationMatrix) + { + shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true); + } + + // bind and sync uniforms.. + renderer.shader.bind(shader); + + // set state.. + renderer.state.setState(this.state); + + // bind the geometry... + renderer.geometry.bind(this.geometry, shader); + + // then render it + renderer.geometry.draw(this.drawMode, this.size, this.start, this.geometry.instanceCount); + } + + /** + * Rendering by using the Batch system. + * @private + * @param {PIXI.Renderer} renderer - Instance to renderer. + */ + _renderToBatch(renderer) + { + const geometry = this.geometry; + + // set properties for batching.. + const vertices = geometry.buffers[0].data; + + if (geometry.vertexDirtyId !== this.vertexDirty || this._transformID !== this.transform._worldID) + { + this._transformID = this.transform._worldID; + + if (this.vertexData.length !== vertices.length) + { + this.vertexData = new Float32Array(vertices.length); + } + + const wt = this.transform.worldTransform; + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const vertexData = this.vertexData; + + for (let i = 0; i < vertexData.length / 2; i++) + { + const x = vertices[(i * 2)]; + const y = vertices[(i * 2) + 1]; + + vertexData[(i * 2)] = (a * x) + (c * y) + tx; + vertexData[(i * 2) + 1] = (b * x) + (d * y) + ty; + } + + this.vertexDirty = geometry.vertexDirtyId; + } + + // set batchable bits.. + this.uvs = geometry.buffers[1].data; + this.indices = geometry.indexBuffer.data; + this._tintRGB = this.shader._tintRGB; + this._texture = this.shader.texture; + + renderer.batch.setObjectRenderer(renderer.plugins.batch); + renderer.plugins.batch.render(this); } /** @@ -118,7 +309,7 @@ } /** - * Tests if a point is inside this mesh. Works only for TRIANGLE_MESH + * Tests if a point is inside this mesh. Works only for PIXI.DRAW_MODES.TRIANGLES. * * @param {PIXI.Point} point the point to test * @return {boolean} the result of the test @@ -160,7 +351,6 @@ return false; } - /** * Destroys the Mesh object. * @@ -168,53 +358,25 @@ * options have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have * their destroy method called as well. 'options' will be passed on to those calls. - * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the texture of the child sprite - * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true - * Should it destroy the base texture of the child sprite */ destroy(options) { - // for each webgl data entry, destroy the WebGLGraphicsData - for (const id in this._glDatas) - { - const data = this._glDatas[id]; - - if (data.destroy) - { - data.destroy(); - } - else - { - if (data.vertexBuffer) - { - data.vertexBuffer.destroy(); - data.vertexBuffer = null; - } - if (data.indexBuffer) - { - data.indexBuffer.destroy(); - data.indexBuffer = null; - } - if (data.uvBuffer) - { - data.uvBuffer.destroy(); - data.uvBuffer = null; - } - if (data.vao) - { - data.vao.destroy(); - data.vao = null; - } - } - } - - this._glDatas = null; + super.destroy(options); this.geometry = null; this.shader = null; this.state = null; - - super.destroy(options); + this.uvs = null; + this.indices = null; + this.vertexData = null; } } + +/** + * The maximum number of vertices to consider batchable. Generally, the complexity + * of the geometry. + * @memberof PIXI.Mesh + * @static + * @member {number} + */ +Mesh.BATCHABLE_SIZE = 100; diff --git a/packages/mesh/src/MeshGeometry.js b/packages/mesh/src/MeshGeometry.js new file mode 100644 index 0000000..ae6c702 --- /dev/null +++ b/packages/mesh/src/MeshGeometry.js @@ -0,0 +1,61 @@ +import { TYPES } from '@pixi/constants'; +import { Buffer, Geometry } from '@pixi/core'; + +/** + * Standard 2D geometry used in PixiJS. + * + * Geometry can be defined without passing in a style or data if required. + * + * ```js + * const geometry = new PIXI.Geometry(); + * + * geometry.addAttribute('positions', [0, 0, 100, 0, 100, 100, 0, 100], 2); + * geometry.addAttribute('uvs', [0,0,1,0,1,1,0,1], 2); + * geometry.addIndex([0,1,2,1,3,2]); + * + * ``` + * @class + * @memberof PIXI + * @extends PIXI.Geometry + */ +export default class MeshGeometry extends Geometry +{ + /** + * @param {Float32Array|number[]} vertices - Positional data on geometry. + * @param {Float32Array|number[]} uvs - Texture UVs. + * @param {Uint16Array|number[]} index - IndexBuffer + */ + constructor(vertices, uvs, index) + { + super(); + + const verticesBuffer = new Buffer(vertices); + const uvsBuffer = new Buffer(uvs, true); + const indexBuffer = new Buffer(index, true, true); + + this.addAttribute('aVertexPosition', verticesBuffer, 2, false, TYPES.FLOAT) + .addAttribute('aTextureCoord', uvsBuffer, 2, false, TYPES.FLOAT) + .addIndex(indexBuffer); + + /** + * Dirty flag to limit update calls on Mesh. For example, + * limiting updates on a single Mesh instance with a shared Geometry + * within the render loop. + * @private + * @member {number} + * @default -1 + */ + this._updateId = -1; + } + + /** + * If the vertex position is updated. + * @member {number} + * @readonly + * @private + */ + get vertexDirtyId() + { + return this.buffers[0]._updateID; + } +} diff --git a/packages/mesh/src/MeshMaterial.js b/packages/mesh/src/MeshMaterial.js new file mode 100644 index 0000000..028cf49 --- /dev/null +++ b/packages/mesh/src/MeshMaterial.js @@ -0,0 +1,130 @@ +import { Shader, Program, TextureMatrix } from '@pixi/core'; +import vertex from './shader/mesh.vert'; +import fragment from './shader/mesh.frag'; +import { Matrix } from '@pixi/math'; +import { premultiplyTintToRgba } from '@pixi/utils'; + +/** + * Slightly opinionated default shader for PixiJS 2D objects. + * @class + * @memberof PIXI + * @extends PIXI.Shader + */ +export default class MeshMaterial extends Shader +{ + /** + * @param {PIXI.Texture} uSampler - Texture that material uses to render. + * @param {object} [options] - Additional options + * @param {number} [options.alpha=1] - Default alpha. + * @param {number} [options.tint=0xFFFFFF] - Default tint. + */ + constructor(uSampler, options) + { + const program = Program.from(vertex, fragment); + + const uniforms = { + uSampler, + alpha: 1, + uTextureMatrix: Matrix.IDENTITY, + uColor: new Float32Array([1, 1, 1, 1]), + }; + + super(program, uniforms); + + /** + * Only do update if tint or alpha changes. + * @member {boolean} + * @private + * @default false + */ + this._colorDirty = false; + + /** + * TextureMatrix instance for this Mesh, used to track Texture changes + * + * @member {PIXI.TextureMatrix} + * @readonly + */ + // TODO get this back in! + this.uvMatrix = new TextureMatrix(this.uSampler); + + /** + * `true` if shader can be batch with the renderer's batch system. + * @member {boolean} + * @default true + */ + this.batchable = true; + + // Set defaults + const { tint, alpha } = Object.assign({ + tint: 0xFFFFFF, + alpha: 1, + }, options); + + this.tint = tint; + this.alpha = alpha; + } + + /** + * Reference to the texture being rendered. + * @member {PIXI.Texture} + */ + get texture() + { + return this.uniforms.uSampler; + } + set texture(value) + { + this.uniforms.uSampler = value; + } + + /** + * This gets automatically set by the object using this. + * @default 1 + * @member {number} + */ + set alpha(value) + { + if (value === this._alpha) return; + + this._alpha = value; + this._colorDirty = true; + } + get alpha() + { + return this._alpha; + } + + /** + * Multiply tint for the material. + * @member {number} + * @default 0xFFFFFF + */ + set tint(value) + { + if (value === this._tint) return; + + this._tint = value; + this._tintRGB = (value >> 16) + (value & 0xff00) + ((value & 0xff) << 16); + this._colorDirty = true; + } + get tint() + { + return this._tint; + } + + /** + * Gets called automatically by the Mesh. Intended to be overridden for custom + * MeshMaterial objects. + */ + update() + { + if (this._colorDirty) + { + this._colorDirty = false; + const baseTexture = this.texture.baseTexture; + + premultiplyTintToRgba(this._tintRGB, this._alpha, this.uniforms.uColor, baseTexture.premultiplyAlpha); + } + } +} diff --git a/packages/mesh/src/MeshRenderer.js b/packages/mesh/src/MeshRenderer.js deleted file mode 100644 index d7baf9b..0000000 --- a/packages/mesh/src/MeshRenderer.js +++ /dev/null @@ -1,77 +0,0 @@ -import { ObjectRenderer } from '@pixi/core'; -import { Matrix } from '@pixi/math'; - -/** - * WebGL renderer plugin for tiling sprites - * - * @class - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class MeshRenderer extends ObjectRenderer -{ - /** - * constructor for renderer - * - * @param {Renderer} renderer The renderer this tiling awesomeness works for. - */ - constructor(renderer) - { - super(renderer); - - this.shader = null; - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - this.gl = this.renderer.gl; - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - } - - /** - * renders mesh - * @private - * @param {PIXI.Mesh} mesh mesh instance - */ - render(mesh) - { - // bind the shader.. - - // TODO - // set the shader props.. - // probably only need to set once! - // as its then a reference.. - if (mesh.shader.program.uniformData.translationMatrix) - { - // the transform! - mesh.shader.uniforms.translationMatrix = mesh.transform.worldTransform.toArray(true); - } - if (mesh.shader.uniforms.uTextureMatrix) - { - if (mesh.uploadUvMatrix) - { - mesh.shader.uniforms.uTextureMatrix = mesh.uvMatrix.mapCoord.toArray(true); - } - else - { - mesh.shader.uniforms.uTextureMatrix = Matrix.IDENTITY.toArray(true); - } - } - - // bind and sync uniforms.. - this.renderer.shader.bind(mesh.shader); - - // set state.. - this.renderer.state.setState(mesh.state); - - // bind the geometry... - this.renderer.geometry.bind(mesh.geometry, mesh.shader); - // then render it - this.renderer.geometry.draw(mesh.drawMode, mesh.size, mesh.start, mesh.geometry.instanceCount); - } -} diff --git a/packages/mesh/src/index.js b/packages/mesh/src/index.js index e14ab9f..9e6dcd8 100644 --- a/packages/mesh/src/index.js +++ b/packages/mesh/src/index.js @@ -1,2 +1,4 @@ export { default as Mesh } from './Mesh'; -export { default as MeshRenderer } from './MeshRenderer'; +export { default as MeshMaterial } from './MeshMaterial'; +export { default as MeshGeometry } from './MeshGeometry'; + diff --git a/packages/mesh/src/shader/mesh.frag b/packages/mesh/src/shader/mesh.frag new file mode 100644 index 0000000..6096983 --- /dev/null +++ b/packages/mesh/src/shader/mesh.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +uniform vec4 uColor; + +uniform sampler2D uSampler; + +void main(void) +{ + gl_FragColor = texture2D(uSampler, vTextureCoord) * uColor; +} diff --git a/packages/mesh/src/shader/mesh.vert b/packages/mesh/src/shader/mesh.vert new file mode 100644 index 0000000..0526df9 --- /dev/null +++ b/packages/mesh/src/shader/mesh.vert @@ -0,0 +1,15 @@ +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; + +uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTextureMatrix; + +varying vec2 vTextureCoord; + +void main(void) +{ + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy; +} diff --git a/packages/sprite/README.md b/packages/sprite/README.md index 3783526..1972ceb 100644 --- a/packages/sprite/README.md +++ b/packages/sprite/README.md @@ -9,8 +9,7 @@ ## Usage ```js -import { SpriteRenderer } from '@pixi/sprite'; -import { Renderer } from '@pixi/core'; +import { Sprite } from '@pixi/sprite'; -Renderer.registerPlugin('sprite', SpriteRenderer); +const sprite = new Sprite(); ``` \ No newline at end of file diff --git a/packages/sprite/src/BatchBuffer.js b/packages/sprite/src/BatchBuffer.js deleted file mode 100644 index cbc4c72..0000000 --- a/packages/sprite/src/BatchBuffer.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @class - * @memberof PIXI - */ -export default class BatchBuffer -{ - /** - * @param {number} size - The size of the buffer in bytes. - */ - constructor(size) - { - this.vertices = new ArrayBuffer(size); - - /** - * View on the vertices as a Float32Array for positions - * - * @member {Float32Array} - */ - this.float32View = new Float32Array(this.vertices); - - /** - * View on the vertices as a Uint32Array for uvs - * - * @member {Float32Array} - */ - this.uint32View = new Uint32Array(this.vertices); - } - - /** - * Destroys the buffer. - * - */ - destroy() - { - this.vertices = null; - this.positions = null; - this.uvs = null; - this.colors = null; - } -} diff --git a/packages/sprite/src/Sprite.js b/packages/sprite/src/Sprite.js index 91546cf..7b66e6e 100644 --- a/packages/sprite/src/Sprite.js +++ b/packages/sprite/src/Sprite.js @@ -5,10 +5,11 @@ import { Container } from '@pixi/display'; const tempPoint = new Point(); +const indices = new Uint16Array([0, 1, 2, 0, 2, 3]); /** * The Sprite object is the base for all textured objects that are rendered to the screen - * +* * A sprite can be created directly from an image like this: * * ```js @@ -119,6 +120,8 @@ */ this.cachedTint = 0xFFFFFF; + this.uvs = null; + // call texture setter this.texture = texture || Texture.EMPTY; @@ -144,6 +147,12 @@ this._transformTrimmedID = -1; this._textureTrimmedID = -1; + // Batchable stuff.. + // TODO could make this a mixin? + this.indices = indices; + this.size = 4; + this.start = 0; + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_render' & '_renderCanvas' methods. @@ -151,7 +160,12 @@ * @member {string} * @default 'sprite' */ - this.pluginName = 'sprite'; + this.pluginName = 'batch'; + + /** + * used to fast check if a sprite is.. a sprite! + */ + this.isSprite = true; } /** @@ -165,6 +179,7 @@ this._textureTrimmedID = -1; this.cachedTint = 0xFFFFFF; + this.uvs = this._texture._uvs.uvsFloat32; // so if _width is 0 then width was not set.. if (this._width) { diff --git a/packages/sprite/src/SpriteRenderer.js b/packages/sprite/src/SpriteRenderer.js deleted file mode 100644 index 136cdcc..0000000 --- a/packages/sprite/src/SpriteRenderer.js +++ /dev/null @@ -1,463 +0,0 @@ -import { Geometry, - Buffer, - ObjectRenderer, - checkMaxIfStatementsInShader } from '@pixi/core'; -import { settings } from '@pixi/settings'; -import { createIndicesForQuads, premultiplyBlendMode, premultiplyTint } from '@pixi/utils'; -import bitTwiddle from 'bit-twiddle'; -import BatchBuffer from './BatchBuffer'; -import generateMultiTextureShader from './generateMultiTextureShader'; -import { ENV } from '@pixi/constants'; - -let TICK = 0; -// const TEXTURE_TICK = 0; - -/** - * Renderer dedicated to drawing and batching sprites. - * - * @class - * @private - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class SpriteRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. - */ - constructor(renderer) - { - super(renderer); - - /** - * Number of values sent in the vertex buffer. - * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 - * - * @member {number} - */ - this.vertSize = 5; - - /** - * The size of the vertex information in bytes. - * - * @member {number} - */ - this.vertByteSize = this.vertSize * 4; - - /** - * The number of images in the SpriteRenderer before it flushes. - * - * @member {number} - */ - this.size = settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop - - // the total number of bytes in our batch - // let numVerts = this.size * 4 * this.vertByteSize; - - this.buffers = []; - for (let i = 1; i <= bitTwiddle.nextPow2(this.size); i *= 2) - { - this.buffers.push(new BatchBuffer(i * 4 * this.vertByteSize)); - } - - /** - * Holds the indices of the geometry (quads) to draw - * - * @member {Uint16Array} - */ - this.indices = createIndicesForQuads(this.size); - this.indexBuffer = new Buffer(this.indices, true, true); - - /** - * The default shaders that is used if a sprite doesn't have a more specific one. - * there is a shader for each number of textures that can be rendered. - * These shaders will also be generated on the fly as required. - * @member {PIXI.Shader[]} - */ - this.shader = null; - - this.currentIndex = 0; - this.groups = []; - - for (let k = 0; k < this.size; k++) - { - this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; - } - - this.sprites = []; - - this.vertexBuffers = []; - this.vaos = []; - - this.vaoMax = 2; - this.vertexCount = 0; - - this.renderer.on('prerender', this.onPrerender, this); - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - contextChange() - { - const gl = this.renderer.gl; - - if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) - { - this.MAX_TEXTURES = 1; - } - else - { - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatementsInShader(this.MAX_TEXTURES, gl); - } - - // generate generateMultiTextureProgram, may be a better move? - this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); - - // we use the second shader as the first one depending on your browser may omit aTextureId - // as it is not used by the shader so is optimized out. - for (let i = 0; i < this.vaoMax; i++) - { - const buffer = new Buffer(null, false); - - /* eslint-disable max-len */ - this.vaos[i] = new Geometry() - .addAttribute('aVertexPosition', buffer, 2, false, gl.FLOAT) - .addAttribute('aTextureCoord', buffer, 2, true, gl.UNSIGNED_SHORT) - .addAttribute('aColor', buffer, 4, true, gl.UNSIGNED_BYTE) - .addAttribute('aTextureId', buffer, 1, true, gl.FLOAT) - .addIndex(this.indexBuffer); - /* eslint-enable max-len */ - - this.vertexBuffers[i] = buffer; - } - } - - /** - * Called before the renderer starts rendering. - * - */ - onPrerender() - { - this.vertexCount = 0; - } - - /** - * Renders the sprite object. - * - * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch - */ - render(sprite) - { - // TODO set blend modes.. - // check texture.. - if (this.currentIndex >= this.size) - { - this.flush(); - } - - // get the uvs for the texture - - // if the uvs have not updated then no point rendering just yet! - if (!sprite._texture._uvs) - { - return; - } - - // push a texture. - // increment the batchsize - this.sprites[this.currentIndex++] = sprite; - } - - /** - * Renders the content and empties the current batch. - * - */ - flush() - { - if (this.currentIndex === 0) - { - return; - } - - const gl = this.renderer.gl; - const MAX_TEXTURES = this.MAX_TEXTURES; - - const np2 = bitTwiddle.nextPow2(this.currentIndex); - const log2 = bitTwiddle.log2(np2); - const buffer = this.buffers[log2]; - - const sprites = this.sprites; - const groups = this.groups; - - const float32View = buffer.float32View; - const uint32View = buffer.uint32View; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let nextTexture; - let currentTexture; - let groupCount = 1; - let textureId = 0; - let textureCount = 0; - let currentGroup = groups[0]; - let vertexData; - let uvs; - let blendMode = premultiplyBlendMode[ - sprites[0]._texture.baseTexture.premultiplyAlpha ? 1 : 0][sprites[0].blendMode]; - - currentGroup.textureCount = 0; - currentGroup.start = 0; - currentGroup.blend = blendMode; - - TICK++; - - let i; - - for (i = 0; i < this.currentIndex; ++i) - { - // upload the sprite elements... - // they have all ready been calculated so we just need to push them into the buffer. - - const sprite = sprites[i]; - - nextTexture = sprite._texture.baseTexture; - textureId = nextTexture._id; - - const spriteBlendMode = premultiplyBlendMode[Number(nextTexture.premultiplyAlpha)][sprite.blendMode]; - - if (blendMode !== spriteBlendMode) - { - blendMode = spriteBlendMode; - - // force the batch to break! - currentTexture = null; - textureCount = MAX_TEXTURES; - TICK++; - } - - if (currentTexture !== nextTexture) - { - currentTexture = nextTexture; - - if (nextTexture._enabled !== TICK) - { - if (textureCount === MAX_TEXTURES) - { - TICK++; - - textureCount = 0; - - currentGroup.size = i - currentGroup.start; - - currentGroup = groups[groupCount++]; - currentGroup.textureCount = 0; - currentGroup.blend = blendMode; - currentGroup.start = i; - } - - nextTexture.touched = touch; - nextTexture._enabled = TICK; - nextTexture._id = textureCount; - - currentGroup.textures[currentGroup.textureCount++] = nextTexture; - textureCount++; - } - } - - vertexData = sprite.vertexData; - - // TODO this sum does not need to be set each frame.. - uvs = sprite._texture._uvs.uvsUint32; - textureId = nextTexture._id; - - if (this.renderer.roundPixels) - { - const resolution = this.renderer.resolution; - - // xy - float32View[index] = ((vertexData[0] * resolution) | 0) / resolution; - float32View[index + 1] = ((vertexData[1] * resolution) | 0) / resolution; - - // xy - float32View[index + 5] = ((vertexData[2] * resolution) | 0) / resolution; - float32View[index + 6] = ((vertexData[3] * resolution) | 0) / resolution; - - // xy - float32View[index + 10] = ((vertexData[4] * resolution) | 0) / resolution; - float32View[index + 11] = ((vertexData[5] * resolution) | 0) / resolution; - - // xy - float32View[index + 15] = ((vertexData[6] * resolution) | 0) / resolution; - float32View[index + 16] = ((vertexData[7] * resolution) | 0) / resolution; - } - else - { - // xy - float32View[index] = vertexData[0]; - float32View[index + 1] = vertexData[1]; - - // xy - float32View[index + 5] = vertexData[2]; - float32View[index + 6] = vertexData[3]; - - // xy - float32View[index + 10] = vertexData[4]; - float32View[index + 11] = vertexData[5]; - - // xy - float32View[index + 15] = vertexData[6]; - float32View[index + 16] = vertexData[7]; - } - - uint32View[index + 2] = uvs[0]; - uint32View[index + 7] = uvs[1]; - uint32View[index + 12] = uvs[2]; - uint32View[index + 17] = uvs[3]; - /* eslint-disable max-len */ - const alpha = Math.min(sprite.worldAlpha, 1.0); - const argb = alpha < 1.0 && nextTexture.premultiplyAlpha ? premultiplyTint(sprite._tintRGB, alpha) - : sprite._tintRGB + (alpha * 255 << 24); - - uint32View[index + 3] = uint32View[index + 8] = uint32View[index + 13] = uint32View[index + 18] = argb; - - float32View[index + 4] = float32View[index + 9] = float32View[index + 14] = float32View[index + 19] = textureId; - /* eslint-enable max-len */ - - index += 20; - } - - currentGroup.size = i - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { - // this is still needed for IOS performance.. - // it really does not like uploading to the same buffer in a single frame! - if (this.vaoMax <= this.vertexCount) - { - this.vaoMax++; - - const buffer = new Buffer(null, false); - - /* eslint-disable max-len */ - this.vaos[this.vertexCount] = new Geometry() - .addAttribute('aVertexPosition', buffer, 2, false, gl.FLOAT) - .addAttribute('aTextureCoord', buffer, 2, true, gl.UNSIGNED_SHORT) - .addAttribute('aColor', buffer, 4, true, gl.UNSIGNED_BYTE) - .addAttribute('aTextureId', buffer, 1, true, gl.FLOAT) - .addIndex(this.indexBuffer); - /* eslint-enable max-len */ - - this.vertexBuffers[this.vertexCount] = buffer; - } - - this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); - this.renderer.geometry.bind(this.vaos[this.vertexCount]); - - this.vertexCount++; - } - else - { - // lets use the faster option, always use buffer number 0 - this.vertexBuffers[this.vertexCount].update(buffer.vertices, 0); - - this.renderer.geometry.updateBuffers(); - } - - // / render the groups.. - for (i = 0; i < groupCount; i++) - { - const group = groups[i]; - const groupTextureCount = group.textureCount; - - for (let j = 0; j < groupTextureCount; j++) - { - this.renderer.texture.bind(group.textures[j], j); - } - - // set the blend mode.. - this.renderer.state.setBlendMode(group.blend); - - gl.drawElements(gl.TRIANGLES, group.size * 6, gl.UNSIGNED_SHORT, group.start * 6 * 2); - } - - // reset elements for the next flush - this.currentIndex = 0; - } - - /** - * Starts a new sprite batch. - */ - start() - { - this.renderer.shader.bind(this.shader); - - if (settings.CAN_UPLOAD_SAME_BUFFER) - { - // bind buffer #0, we don't need others - this.renderer.geometry.bind(this.vaos[this.vertexCount]); - } - } - - /** - * Stops and flushes the current batch. - * - */ - stop() - { - this.flush(); - } - - /** - * Destroys the SpriteRenderer. - * - */ - destroy() - { - for (let i = 0; i < this.vaoMax; i++) - { - if (this.vertexBuffers[i]) - { - this.vertexBuffers[i].destroy(); - } - if (this.vaos[i]) - { - this.vaos[i].destroy(); - } - } - - if (this.indexBuffer) - { - this.indexBuffer.destroy(); - } - - this.renderer.off('prerender', this.onPrerender, this); - - if (this.shader) - { - this.shader.destroy(); - this.shader = null; - } - - this.vertexBuffers = null; - this.vaos = null; - this.indexBuffer = null; - this.indices = null; - - this.sprites = null; - - for (let i = 0; i < this.buffers.length; ++i) - { - this.buffers[i].destroy(); - } - - super.destroy(); - } -} diff --git a/packages/sprite/src/generateMultiTextureShader.js b/packages/sprite/src/generateMultiTextureShader.js deleted file mode 100644 index f2e27be..0000000 --- a/packages/sprite/src/generateMultiTextureShader.js +++ /dev/null @@ -1,69 +0,0 @@ -import { Shader, UniformGroup } from '@pixi/core'; -import vertex from './texture.vert'; - -const fragTemplate = [ - 'varying vec2 vTextureCoord;', - 'varying vec4 vColor;', - 'varying float vTextureId;', - 'uniform sampler2D uSamplers[%count%];', - - 'void main(void){', - 'vec4 color;', - 'float textureId = floor(vTextureId+0.5);', - '%forloop%', - 'gl_FragColor = color * vColor;', - '}', -].join('\n'); - -export default function generateMultiTextureShader(gl, maxTextures) -{ - const sampleValues = new Int32Array(maxTextures); - - for (let i = 0; i < maxTextures; i++) - { - sampleValues[i] = i; - } - - const uniforms = { - default: UniformGroup.from({ uSamplers: sampleValues }, true), - }; - - let fragmentSrc = fragTemplate; - - fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); - fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); - - const shader = Shader.from(vertex, fragmentSrc, uniforms); - - return shader; -} - -function generateSampleSrc(maxTextures) -{ - let src = ''; - - src += '\n'; - src += '\n'; - - for (let i = 0; i < maxTextures; i++) - { - if (i > 0) - { - src += '\nelse '; - } - - if (i < maxTextures - 1) - { - src += `if(textureId == ${i}.0)`; - } - - src += '\n{'; - src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; - src += '\n}'; - } - - src += '\n'; - src += '\n'; - - return src; -} diff --git a/packages/sprite/src/index.js b/packages/sprite/src/index.js index edf477c..c5179d7 100644 --- a/packages/sprite/src/index.js +++ b/packages/sprite/src/index.js @@ -1,2 +1 @@ export { default as Sprite } from './Sprite'; -export { default as SpriteRenderer } from './SpriteRenderer'; diff --git a/packages/sprite/src/texture.vert b/packages/sprite/src/texture.vert deleted file mode 100644 index 18b89ff..0000000 --- a/packages/sprite/src/texture.vert +++ /dev/null @@ -1,19 +0,0 @@ -precision highp float; -attribute vec2 aVertexPosition; -attribute vec2 aTextureCoord; -attribute vec4 aColor; -attribute float aTextureId; - -uniform mat3 projectionMatrix; - -varying vec2 vTextureCoord; -varying vec4 vColor; -varying float vTextureId; - -void main(void){ - gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - - vTextureCoord = aTextureCoord; - vTextureId = aTextureId; - vColor = aColor; -} diff --git a/packages/sprite/test/SpriteRenderer.js b/packages/sprite/test/SpriteRenderer.js deleted file mode 100644 index 35758ad..0000000 --- a/packages/sprite/test/SpriteRenderer.js +++ /dev/null @@ -1,43 +0,0 @@ -const { SpriteRenderer } = require('../'); - -const mockrunner = { - contextChange: { - remove: () => 1, - add: () => 1, - }, -}; - -describe('SpriteRenderer', function () -{ - it('can be destroyed', function () - { - const destroyable = { destroy: sinon.stub() }; - const webgl = { - on: sinon.stub(), - runners: mockrunner, - off: sinon.stub(), - }; - const renderer = new SpriteRenderer(webgl); - - // simulate onContextChange - renderer.vertexBuffers = [destroyable, destroyable]; - renderer.vaos = [destroyable, destroyable]; - renderer.indexBuffer = destroyable; - renderer.shader = destroyable; - - expect(() => renderer.destroy()).to.not.throw(); - }); - - it('can be destroyed immediately', function () - { - const webgl = { - on: sinon.stub(), - runners: mockrunner, - off: sinon.stub(), - }; - - const renderer = new SpriteRenderer(webgl); - - expect(() => renderer.destroy()).to.not.throw(); - }); -}); diff --git a/packages/sprite/test/index.js b/packages/sprite/test/index.js index 76e90bc..b5ea071 100644 --- a/packages/sprite/test/index.js +++ b/packages/sprite/test/index.js @@ -1,2 +1 @@ require('./Sprite'); -require('./SpriteRenderer'); diff --git a/tools/integration-tests/package.json b/tools/integration-tests/package.json index 44e2bed..712d250 100644 --- a/tools/integration-tests/package.json +++ b/tools/integration-tests/package.json @@ -18,8 +18,10 @@ "@pixi/graphics": "^5.0.0-alpha.3", "@pixi/math": "^5.0.0-alpha.3", "@pixi/mesh": "^5.0.0-alpha.3", + "@pixi/mesh-extras": "^5.0.0-alpha.3", "@pixi/sprite": "^5.0.0-alpha.3", "@pixi/text": "^5.0.0-alpha.3", + "@pixi/utils": "^5.0.0-alpha.3", "floss": "^2.1.3" } } diff --git a/tools/integration-tests/test/display/getLocalBounds.js b/tools/integration-tests/test/display/getLocalBounds.js index b7269b0..905a180 100644 --- a/tools/integration-tests/test/display/getLocalBounds.js +++ b/tools/integration-tests/test/display/getLocalBounds.js @@ -6,8 +6,9 @@ const { Graphics } = require('@pixi/graphics'); const { CanvasGraphicsRenderer } = require('@pixi/canvas-graphics'); const { Text } = require('@pixi/text'); -const { Plane } = require('@pixi/mesh'); +const { SimplePlane } = require('@pixi/mesh-extras'); const { CanvasMeshRenderer } = require('@pixi/canvas-mesh'); +const { isWebGLSupported } = require('@pixi/utils'); require('@pixi/canvas-display'); @@ -15,6 +16,11 @@ CanvasRenderer.registerPlugin('graphics', CanvasGraphicsRenderer); CanvasRenderer.registerPlugin('mesh', CanvasMeshRenderer); +function withGL(fn) +{ + return isWebGLSupported() ? fn : undefined; +} + describe('getLocalBounds', function () { it('should register correct local-bounds with a LOADED Sprite', function () @@ -165,13 +171,13 @@ expect(bounds.height).to.equal(10); }); - it.skip('should register correct local-bounds with a Mesh', function () + it('should register correct local-bounds with a Mesh', withGL(function () { const parent = new Container(); const texture = RenderTexture.create(10, 10); - const plane = new Plane(texture); + const plane = new SimplePlane(texture); parent.addChild(plane); @@ -184,7 +190,7 @@ expect(bounds.y).to.equal(0); expect(bounds.width).to.equal(10); expect(bounds.height).to.equal(10); - }); + })); it('should register correct local-bounds with a cachAsBitmap item inside after a render', function () {