const PIXI = require('pixi.js')
const Angle = require('yy-angle')
/**
* pixi-pixelate: a container to create proper pixelated graphics
*/
class Pixelate extends PIXI.Container
{
constructor()
{
super()
this.cursor = { x: 0, y: 0 }
this.tint = 0xffffff
this._lineStyle = { width: 1, tint: 0xffffff, alpha: 1, direction: 'up' }
this.cache = []
}
/**
* clear all graphics
* @returns {Pixelate}
*/
clear()
{
while (this.children.length)
{
this.cache.push(this.children.pop())
}
return this
}
/**
* texture to use for sprites (defaults to PIXI.Texture.WHITE)
* @type {PIXI.Texture}
*/
static get texture()
{
return Pixelate._texture
}
static set texture(value)
{
Pixelate._texture = value
}
/**
* creates or gets an old sprite
* @param {number} tint
* @param {number} alpha
* @private
*/
getPoint(tint, alpha)
{
let point
if (this.cache.length)
{
point = this.addChild(this.cache.pop())
}
else
{
point = this.addChild(new PIXI.Sprite(Pixelate.texture))
}
point.tint = typeof tint === 'undefined' ? this._lineStyle.tint : tint
point.alpha = typeof alpha === 'undefined' ? this._lineStyle.alpha : alpha
return point
}
/**
* draw a list of points
* @param {(number[]|PIXI.Point[]|PIXI.PointLike[])} points
* @param {number} tint
* @param {number} alpha
*/
points(points, tint, alpha)
{
if (isNaN(points[0]))
{
for (let point of points)
{
this.point(point.x, point.y, tint, alpha)
}
}
else
{
for (let i = 0; i < points.length; i += 2)
{
this.point(points[i], points[i + 1], tint, alpha)
}
}
}
/**
* add a point using lineStyle or provided tint and alpha
* @param {number} x
* @param {number} y
* @param {number} [tint]
* @param {number} [alpha]
* @returns {Pixelate}
*/
point(x, y, tint, alpha)
{
const point = this.getPoint(tint, alpha)
point.position.set(x, y)
point.width = point.height = 1
return this
}
/**
* if lineStyle.width > 1 then use this direction to place the next line; center=alternate up and down
* @typedef {string} LineDirection (up, center, down)
*/
/**
* set linestyle for pixelated layer
* NOTE: width only works for line() for now
* @param {number} width
* @param {number} [tint=0xffffff]
* @param {number} [alpha=1]
* @param {LineDirection} [direction=up] (up, center, down)
* @returns {Pixelate}
*/
lineStyle(width, tint, alpha, direction)
{
this._lineStyle.width = width
this._lineStyle.tint = typeof tint !== 'undefined' ? tint : 0xffffff
this._lineStyle.alpha = typeof alpha !== 'undefined' ? alpha : 1
this._lineStyle.direction = direction || 'up'
return this
}
/**
* move cursor to this location
* @param {number} x
* @param {number} y
* @returns {Pixelate}
*/
moveTo(x, y)
{
this.cursor.x = x
this.cursor.y = y
return this
}
/**
* draw a pixelated line between two points and move cursor to the second point
* @param {number} x0
* @param {number} y0
* @param {number} x1
* @param {number} y1
* @param {number} [tint]
* @param {number} [alpha]
* @param {number} [lineWidth]
* @param {LineDirection} [lineDirection]
* @returns {Pixelate}
*/
line(x0, y0, x1, y1, tint, alpha, lineWidth, lineDirection)
{
lineWidth = typeof lineWidth === 'undefined' ? this._lineStyle.width : lineWidth
lineDirection = lineDirection || this._lineStyle.direction
if (lineWidth === 1)
{
this.drawPoints(this.linePoints(x0, y0, x1, y1), tint, alpha)
}
else
{
const angle = Angle.angleTwoPoints(x0, y0, x1, y1) + Math.PI / 2 * (lineDirection === 'up' ? -1 : 1)
const cos = Math.cos(angle)
const sin = Math.sin(angle)
const points = []
if (lineDirection === 'center')
{
const half = lineWidth / 2
points.push(x0 + Math.round(cos * half), y0 + Math.round(sin * half))
points.push(x1 + Math.round(cos * half), y1 + Math.round(sin * half))
points.push(x1 - Math.round(cos * half), y1 - Math.round(sin * half))
points.push(x0 - Math.round(cos * half), y0 - Math.round(sin * half))
}
else
{
points.push(x0, y0)
points.push(x0 + Math.round(cos * lineWidth), y0 + Math.round(sin * lineWidth))
points.push(x1 + Math.round(cos * lineWidth), y1 + Math.round(sin * lineWidth))
points.push(x1, y1)
}
this.polygonFill(points, tint, alpha, 1)
}
return this
}
/**
* draw a pixelated line between two points and move cursor to the second point
* based on https://github.com/madbence/node-bresenham/blob/master/index.js
* @private
* @param {number} x0
* @param {number} y0
* @param {number} x1
* @param {number} y1
* @param {number[]} [points]
* @returns {number[]}
*/
linePoints(x0, y0, x1, y1, points)
{
points = points || []
points.push([x0, y0])
var dx = x1 - x0;
var dy = y1 - y0;
var adx = Math.abs(dx);
var ady = Math.abs(dy);
var eps = 0;
var sx = dx > 0 ? 1 : -1;
var sy = dy > 0 ? 1 : -1;
if (adx > ady)
{
for (var x = x0, y = y0; sx < 0 ? x >= x1 : x <= x1; x += sx)
{
points.push([x, y])
eps += ady;
if ((eps << 1) >= adx)
{
y += sy;
eps -= adx;
}
}
} else
{
for (var x = x0, y = y0; sy < 0 ? y >= y1 : y <= y1; y += sy)
{
points.push([x, y])
eps += adx;
if ((eps << 1) >= ady)
{
x += sx;
eps -= ady;
}
}
}
return points
}
/**
* create a unique array
* from https://stackoverflow.com/a/9229821/1955997
* @private
* @param {Array} a
*/
hashUnique(a)
{
const seen = {}
return a.filter((item) =>
{
const key = item[0] + '.' + item[1]
return seen.hasOwnProperty(key) ? false : (seen[key] = true)
})
}
/**
* draw a set of points, removing duplicates first
* @private
* @param {object[]}
*/
drawPoints(points, tint, alpha)
{
points = this.hashUnique(points)
for (let point of points)
{
this.point(point[0], point[1], tint, alpha)
}
}
/**
* draw a pixelated line from the cursor position to this position
* @param {number} x
* @param {number} y
* @returns {Pixelate}
*/
lineTo(x, y)
{
this.drawPoints(this.linePoints(this.cursor.x, this.cursor.y, x, y))
this.cursor.x = x
this.cursor.y = y
return this
}
/**
* draw a pixelated circle
* from https://en.wikipedia.org/wiki/Midpoint_circle_algorithm
* @param {number} x0
* @param {number} y0
* @param {number} radius
* @param {number} [tint]
* @param {number} [alpha]
* @returns {Pixelate}
*/
circle(x0, y0, radius, tint, alpha)
{
const points = []
let x = radius
let y = 0
let decisionOver2 = 1 - x // Decision criterion divided by 2 evaluated at x=r, y=0
while (x >= y)
{
points.push([x + x0, y + y0])
points.push([y + x0, x + y0])
points.push([-x + x0, y + y0])
points.push([-y + x0, x + y0])
points.push([-x + x0, -y + y0])
points.push([-y + x0, -x + y0])
points.push([x + x0, -y + y0])
points.push([y + x0, -x + y0])
y++
if (decisionOver2 <= 0)
{
decisionOver2 += 2 * y + 1 // Change in decision criterion for y -> y+1
} else
{
x--
decisionOver2 += 2 * (y - x) + 1 // Change for y -> y+1, x -> x-1
}
}
this.drawPoints(points, tint, alpha)
return this
}
/**
* draw and fill circle
* @param {number} x center
* @param {number} y center
* @param {number} radius
* @param {number} tint
* @param {number} alpha
*/
circleFill(x0, y0, radius, tint, alpha)
{
const points = []
let x = radius
let y = 0
let decisionOver2 = 1 - x // Decision criterion divided by 2 evaluated at x=r, y=0
while (x >= y)
{
this.rectPoints(-x + x0, y + y0, x * 2 + 1, 1, points)
this.rectPoints(-y + x0, x + y0, y * 2 + 1, 1, points)
this.rectPoints(-x + x0, -y + y0, x * 2 + 1, 1, points)
this.rectPoints(-y + x0, -x + y0, y * 2 + 1, 1, points)
y++
if (decisionOver2 <= 0)
{
decisionOver2 += 2 * y + 1 // Change in decision criterion for y -> y+1
} else
{
x--
decisionOver2 += 2 * (y - x) + 1 // Change for y -> y+1, x -> x-1
}
}
this.drawPoints(points, tint, alpha)
return this
}
/**
* return an array of points for a rect
* @private
* @param {number} x0
* @param {number} y0
* @param {number} width
* @param {number} height
* @param {number[]} [points]
* @returns {object[]}
*/
rectPoints(x0, y0, width, height, points)
{
points = points || []
for (let y = y0; y < y0 + height; y++)
{
for (let x = x0; x < x0 + width; x++)
{
points.push([x, y])
}
}
return points
}
/**
* draw the outline of a rect
* @param {number} x
* @param {number} y
* @param {number} width
* @param {number} height
* @param {number} tint
* @param {number} alpha
* @return {Pixelate}
*/
rect(x, y, width, height, tint, alpha)
{
if (width === 1)
{
const point = this.getPoint(tint, alpha)
point.position.set(x, y)
point.width = 1
point.height = height
}
else if (height === 1)
{
const point = this.getPoint(tint, alpha)
point.position.set(x, y)
point.width = 1
point.height = 1
}
else
{
const top = this.getPoint(tint, alpha)
top.position.set(x, y)
top.width = width + 1
top.height = 1
const bottom = this.getPoint(tint, alpha)
bottom.position.set(x, y + height)
bottom.width = width + 1
bottom.height = 1
const left = this.getPoint(tint, alpha)
left.position.set(x, y + 1)
left.width = 1
left.height = height - 1
const right = this.getPoint(tint, alpha)
right.position.set(x + width, y + 1)
right.width = 1
right.height = height - 1
}
return this
}
/**
* draw and fill rectangle
* @param {number} x
* @param {number} y
* @param {number} width
* @param {number} height
* @param {number} [tint]
* @param {number} [alpha]
* @returns {Pixelate}
*/
rectFill(x, y, width, height, tint, alpha)
{
const point = this.getPoint(tint, alpha)
point.position.set(x, y)
point.width = width + 1
point.height = height + 1
return this
}
/**
* draw a pixelated ellipse
* from http://cfetch.blogspot.tw/2014/01/wap-to-draw-ellipse-using-midpoint.html
* @param {number} xc center
* @param {number} yc center
* @param {number} rx - radius x-axis
* @param {number} ry - radius y-axis
* @param {number} tint
* @param {number} alpha
* @returns {Pixelate}
*/
ellipse(xc, yc, rx, ry, tint, alpha)
{
const points = []
let x = 0, y = ry
let p = (ry * ry) - (rx * rx * ry) + ((rx * rx) / 4)
while ((2 * x * ry * ry) < (2 * y * rx * rx))
{
points.push([xc + x, yc - y])
points.push([xc - x, yc + y])
points.push([xc + x, yc + y])
points.push([xc - x, yc - y])
if (p < 0)
{
x = x + 1
p = p + (2 * ry * ry * x) + (ry * ry)
}
else
{
x = x + 1
y = y - 1
p = p + (2 * ry * ry * x + ry * ry) - (2 * rx * rx * y)
}
}
p = (x + 0.5) * (x + 0.5) * ry * ry + (y - 1) * (y - 1) * rx * rx - rx * rx * ry * ry
while (y >= 0)
{
points.push([xc + x, yc - y])
points.push([xc - x, yc + y])
points.push([xc + x, yc + y])
points.push([xc - x, yc - y])
if (p > 0)
{
y = y - 1
p = p - (2 * rx * rx * y) + (rx * rx)
}
else
{
y = y - 1
x = x + 1
p = p + (2 * ry * ry * x) - (2 * rx * rx * y) - (rx * rx)
}
}
this.drawPoints(points, tint, alpha)
return this
}
/**
* draw and fill ellipse
* @param {number} xc - x-center
* @param {number} yc - y-center
* @param {number} rx - radius x-axis
* @param {number} ry - radius y-axis
* @param {number} tint
* @returns {Pixelate}
*/
ellipseFill(xc, yc, rx, ry, tint, alpha)
{
const points = []
let x = 0, y = ry
let p = (ry * ry) - (rx * rx * ry) + ((rx * rx) / 4)
while ((2 * x * ry * ry) < (2 * y * rx * rx))
{
this.rectPoints(xc - x, yc - y, x * 2 + 1, 1, points)
this.rectPoints(xc - x, yc + y, x * 2 + 1, 1, points)
if (p < 0)
{
x = x + 1
p = p + (2 * ry * ry * x) + (ry * ry)
}
else
{
x = x + 1
y = y - 1
p = p + (2 * ry * ry * x + ry * ry) - (2 * rx * rx * y)
}
}
p = (x + 0.5) * (x + 0.5) * ry * ry + (y - 1) * (y - 1) * rx * rx - rx * rx * ry * ry
while (y >= 0)
{
this.rectPoints(xc - x, yc - y, x * 2 + 1, 1, points)
this.rectPoints(xc - x, yc + y, x * 2 + 1, 1, points)
if (p > 0)
{
y = y - 1
p = p - (2 * rx * rx * y) + (rx * rx)
}
else
{
y = y - 1
x = x + 1
p = p + (2 * ry * ry * x) - (2 * rx * rx * y) - (rx * rx)
}
}
this.drawPoints(points, tint, alpha)
return this
}
/**
* draw a pixelated polygon
* @param {number[]} vertices
* @param {number} tint
* @param {number} alpha
* @returns {Pixelate}
*/
polygon(vertices, tint, alpha)
{
const points = []
for (let i = 2; i < vertices.length; i += 2)
{
this.linePoints(vertices[i - 2], vertices[i - 1], vertices[i], vertices[i + 1], points)
}
if (vertices[vertices.length - 2] !== vertices[0] || vertices[vertices.length - 1] !== vertices[1])
{
this.linePoints(vertices[vertices.length - 2], vertices[vertices.length - 1], vertices[0], vertices[1], points)
}
this.drawPoints(points, tint, alpha)
}
/**
* draw and fill pixelated polygon
* @param {number[]} vertices
* @param {number} tint
* @param {number} alpha
* @returns {Pixelate}
*/
polygonFill(vertices, tint, alpha)
{
function mod(n, m)
{
return ((n % m) + m) % m
}
const points = []
const edges = [], active = []
let minY = Infinity, maxY = 0
for (let i = 0; i < vertices.length; i += 2)
{
const p1 = { x: vertices[i], y: vertices[i + 1] }
const p2 = { x: vertices[mod(i + 2, vertices.length)], y: vertices[mod(i + 3, vertices.length)] }
if (p1.y - p2.y !== 0)
{
const edge = {}
edge.p1 = p1
edge.p2 = p2
if (p1.y < p2.y)
{
edge.minY = p1.y
edge.minX = p1.x
}
else
{
edge.minY = p2.y
edge.minX = p2.x
}
minY = (edge.minY < minY) ? edge.minY : minY
edge.maxY = Math.max(p1.y, p2.y)
maxY = (edge.maxY > maxY) ? edge.maxY : maxY
if (p1.x - p2.x === 0)
{
edge.slope = Infinity
edge.b = p1.x
}
else
{
edge.slope = (p1.y - p2.y) / (p1.x - p2.x)
edge.b = p1.y - edge.slope * p1.x
}
edges.push(edge)
}
}
edges.sort((a, b) => { return a.minY - b.minY })
for (let y = minY; y <= maxY; y++)
{
for (let i = 0; i < edges.length; i++)
{
const edge = edges[i]
if (edge.minY === y)
{
active.push(edge)
edges.splice(i, 1)
i--
}
}
for (let i = 0; i < active.length; i++)
{
const edge = active[i]
if (edge.maxY < y)
{
active.splice(i, 1)
i--
}
else
{
if (edge.slope !== Infinity)
{
edge.x = Math.round((y - edge.b) / edge.slope)
}
else
{
edge.x = edge.b
}
}
}
if (active.length)
{
active.sort((a, b) => { return a.x - b.x === 0 ? b.maxY - a.maxY : a.x - b.x })
let bit = true, current = 1
for (let x = active[0].x; x <= active[active.length - 1].x; x++)
{
if (bit)
{
points.push([x, y])
}
if (active[current].x === x)
{
if (active[current].maxY !== y)
{
bit = !bit
}
current++
}
}
}
else
{
return this
}
}
this.drawPoints(points, tint, alpha)
return this
}
/**
* draw arc
* @param {number} x0 - x-start
* @param {number} y0 - y-start
* @param {number} radius - radius
* @param {number} start angle (radians)
* @param {number} end angle (radians)
* @param {number} tint
* @param {number} alpha
* @returns {Pixelate}
*/
arc(x0, y0, radius, start, end, tint, alpha)
{
const interval = Math.PI / radius / 4
const points = []
for (let i = start; i <= end; i += interval)
{
points.push([Math.floor(x0 + Math.cos(i) * radius), Math.floor(y0 + Math.sin(i) * radius)])
}
this.drawPoints(points, tint, alpha)
return this
}
/**
* empties cache of old sprites
*/
flush()
{
this.cache = []
}
}
Pixelate._texture = PIXI.Texture.WHITE
module.exports = Pixelate