import { SHAPES } from '@pixi/math';
import { Bounds } from '@pixi/display';
import { BatchGeometry, BatchDrawCall, BaseTexture } from '@pixi/core';
import { DRAW_MODES } from '@pixi/constants';
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';
import { premultiplyTint } from '@pixi/utils';
const BATCH_POOL = [];
const DRAW_CALL_POOL = [];
/**
* 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;
/**
* A little internal structure to hold interim batch objects.
*
* @private
*/
class BatchPart
{
constructor()
{
this.style = null;
this.size = 0;
this.start = 0;
this.attribStart = 0;
this.attribSize = 0;
}
}
/**
* 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 updating 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[]}
* @protected
*/
this.points = [];
/**
* The collection of colors
*
* @member {number[]}
* @protected
*/
this.colors = [];
/**
* The UVs collection
*
* @member {number[]}
* @protected
*/
this.uvs = [];
/**
* The indices of the vertices
*
* @member {number[]}
* @protected
*/
this.indices = [];
/**
* Reference to the texture IDs.
*
* @member {number[]}
* @protected
*/
this.textureIds = [];
/**
* The collection of drawn shapes.
*
* @member {PIXI.GraphicsData[]}
* @protected
*/
this.graphicsData = [];
/**
* Used to detect if the graphics object has changed. If this is set to true then the graphics
* object will be recalculated.
*
* @member {number}
* @protected
*/
this.dirty = 0;
/**
* Batches need to regenerated if the geometry is updated.
*
* @member {number}
* @protected
*/
this.batchDirty = -1;
/**
* Used to check if the cache is dirty.
*
* @member {number}
* @protected
*/
this.cacheDirty = -1;
/**
* Used to detect if we clear the graphics WebGL data.
*
* @member {number}
* @default 0
* @protected
*/
this.clearDirty = 0;
/**
* List of current draw calls drived from the batches.
*
* @member {object[]}
* @protected
*/
this.drawCalls = [];
/**
* Intermediate abstract format sent to batch system.
* Can be converted to drawCalls or to batchable objects.
*
* @member {object[]}
* @protected
*/
this.batches = [];
/**
* Index of the current last shape in the stack of calls.
*
* @member {number}
* @protected
*/
this.shapeIndex = 0;
/**
* Cached bounds.
*
* @member {PIXI.Bounds}
* @protected
*/
this._bounds = new Bounds();
/**
* The bounds dirty flag.
*
* @member {number}
* @protected
*/
this.boundsDirty = -1;
/**
* Padding to add to the bounds.
*
* @member {number}
* @default 0
*/
this.boundsPadding = 0;
this.batchable = false;
this.indicesUint16 = null;
this.uvsFloat32 = null;
}
/**
* Get the current bounds of the graphic geometry.
*
* @member {PIXI.Bounds}
* @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.batchDirty++;
this.graphicsData.length = 0;
this.shapeIndex = 0;
this.points.length = 0;
this.colors.length = 0;
this.uvs.length = 0;
this.indices.length = 0;
this.textureIds.length = 0;
for (let i = 0; i < this.drawCalls.length; i++)
{
this.drawCalls[i].textures.length = 0;
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.GraphicsGeometry} 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.GraphicsGeometry} 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];
data.lineStyle = lastShape.lineStyle;
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.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.
* @protected
*/
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()
|| new BatchPart();
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() || new BatchPart();
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.processHoles(data.holes);
buildPoly.triangulate(data, this);
}
else
{
command.triangulate(data, this);
}
}
else
{
buildLine(data, this);
for (let i = 0; i < data.holes.length; i++)
{
buildLine(data.holes[i], 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.isBatchable();
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();
}
}
/**
* Checks to see if this graphics geometry can be batched.
* Currently it needs to be small enough and not contain any native lines.
* @protected
*/
isBatchable()
{
const batches = this.batches;
for (let i = 0; i < batches.length; i++)
{
if (batches[i].style.native)
{
return false;
}
}
return (this.points.length < GraphicsGeometry.BATCHABLE_SIZE * 2);
}
/**
* Converts intermediate batches data to drawCalls.
* @protected
*/
buildDrawCalls()
{
let TICK = ++BaseTexture._globalBatch;
for (let i = 0; i < this.drawCalls.length; i++)
{
this.drawCalls[i].textures.length = 0;
DRAW_CALL_POOL.push(this.drawCalls[i]);
}
this.drawCalls.length = 0;
const uvs = this.uvs;
const colors = this.colors;
const textureIds = this.textureIds;
let currentGroup = DRAW_CALL_POOL.pop() || new BatchDrawCall();
currentGroup.textureCount = 0;
currentGroup.start = 0;
currentGroup.size = 0;
currentGroup.type = DRAW_MODES.TRIANGLES;
let textureCount = 0;
let currentTexture = null;
let textureId = 0;
let native = false;
let drawMode = DRAW_MODES.TRIANGLES;
let index = 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 (native !== style.native)
{
native = style.native;
drawMode = native ? DRAW_MODES.LINES : DRAW_MODES.TRIANGLES;
// force the batch to break!
currentTexture = null;
textureCount = MAX_TEXTURES;
TICK++;
}
if (currentTexture !== nextTexture)
{
currentTexture = nextTexture;
if (nextTexture._batchEnabled !== TICK)
{
if (textureCount === MAX_TEXTURES)
{
TICK++;
textureCount = 0;
if (currentGroup.size > 0)
{
currentGroup = DRAW_CALL_POOL.pop() || new BatchDrawCall();
this.drawCalls.push(currentGroup);
}
currentGroup.start = index;
currentGroup.size = 0;
currentGroup.textureCount = 0;
currentGroup.type = drawMode;
}
// TODO add this to the render part..
nextTexture.touched = 1;// touch;
nextTexture._batchEnabled = TICK;
nextTexture._id = textureCount;
nextTexture.wrapMode = 10497;
currentGroup.textures[currentGroup.textureCount++] = nextTexture;
textureCount++;
}
}
currentGroup.size += data.size;
index += data.size;
textureId = nextTexture._id;
this.addColors(colors, style.color, style.alpha, data.attribSize);
this.addTextureIds(textureIds, textureId, data.attribSize);
}
BaseTexture._globalBatch = TICK;
// 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
* @protected
*/
processHoles(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.
* @protected
*/
calculateBounds()
{
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
if (this.graphicsData.length)
{
let shape = null;
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.
*
* @protected
* @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.
*
* @protected
* @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 rgb = (color >> 16) + (color & 0xff00) + ((color & 0xff) << 16);
const rgba = premultiplyTint(rgb, alpha);
while (size-- > 0)
{
colors.push(rgba);
}
}
/**
* Add texture id that the shader/fragment wants to use.
*
* @protected
* @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.
*
* @protected
* @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;
const uvsStart = uvs.length;
const frame = texture.frame;
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++;
uvs.push(x / frame.width, y / frame.height);
}
const baseTexture = texture.baseTexture;
if (frame.width < baseTexture.width
|| frame.height < baseTexture.height)
{
this.adjustUvs(uvs, texture, uvsStart, size);
}
}
/**
* Modify uvs array according to position of texture region
* Does not work with rotated or trimmed textures
* @param {number} uvs array
* @param {PIXI.Texture} texture region
* @param {number} start starting index for uvs
* @param {number} size how many points to adjust
*/
adjustUvs(uvs, texture, start, size)
{
const baseTexture = texture.baseTexture;
const eps = 1e-6;
const finish = start + (size * 2);
const frame = texture.frame;
const scaleX = frame.width / baseTexture.width;
const scaleY = frame.height / baseTexture.height;
let offsetX = frame.x / frame.width;
let offsetY = frame.y / frame.height;
let minX = Math.floor(uvs[start] + eps);
let minY = Math.floor(uvs[start + 1] + eps);
for (let i = start + 2; i < finish; i += 2)
{
minX = Math.min(minX, Math.floor(uvs[i] + eps));
minY = Math.min(minY, Math.floor(uvs[i + 1] + eps));
}
offsetX -= minX;
offsetY -= minY;
for (let i = start; i < finish; i += 2)
{
uvs[i] = (uvs[i] + offsetX) * scaleX;
uvs[i + 1] = (uvs[i + 1] + offsetY) * scaleY;
}
}
}
/**
* 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} BATCHABLE_SIZE
* @default 100
*/
GraphicsGeometry.BATCHABLE_SIZE = 100;