pixi-dashed-line2
Version:
A pixi.js implementation to support dashed lines in PIXI.Graphics.
383 lines (348 loc) • 13.8 kB
text/typescript
import * as PIXI from 'pixi.js'
/** Define the dash: [dash length, gap size, dash size, gap size, ...] */
export type Dashes = number[]
export interface DashLineOptions {
dash?: Dashes,
width?: number
color?: number
alpha?: number
scale?: number
useTexture?: boolean
useDots?: boolean
cap?: PIXI.LINE_CAP
join?: PIXI.LINE_JOIN
alignment?: number
}
const dashLineOptionsDefault: Partial<DashLineOptions> = {
dash: [10, 5],
width: 1,
color: 0xffffff,
alpha: 1,
scale: 1,
useTexture: false,
alignment: 0.5,
}
export class DashLine {
graphics: PIXI.Graphics
/** current length of the line */
lineLength: number
/** cursor location */
cursor = new PIXI.Point()
/** desired scale of line */
scale = 1
// sanity check to ensure the lineStyle is still in use
private activeTexture: PIXI.Texture
private start: PIXI.Point
private dashSize: number
private dash: number[]
private useTexture: boolean
private options: DashLineOptions;
// cache of PIXI.Textures for dashed lines
static dashTextureCache: Record<string, PIXI.Texture> = {}
/**
* Create a DashLine
* @param graphics
* @param [options]
* @param [options.useTexture=false] - use the texture based render (useful for very large or very small dashed lines)
* @param [options.dashes=[10,5] - an array holding the dash and gap (eg, [10, 5, 20, 5, ...])
* @param [options.width=1] - width of the dashed line
* @param [options.alpha=1] - alpha of the dashed line
* @param [options.color=0xffffff] - color of the dashed line
* @param [options.cap] - add a PIXI.LINE_CAP style to dashed lines (only works for useTexture: false)
* @param [options.join] - add a PIXI.LINE_JOIN style to the dashed lines (only works for useTexture: false)
* @param [options.alignment] - The alignment of any lines drawn (0.5 = middle, 1 = outer, 0 = inner)
*/
constructor(graphics: PIXI.Graphics, options: DashLineOptions = {}) {
this.graphics = graphics
options = { ...dashLineOptionsDefault, ...options }
this.dash = options.dash
this.dashSize = this.dash.reduce((a, b) => a + b)
this.useTexture = options.useTexture
this.options = options;
this.setLineStyle();
}
/** resets line style to enable dashed line (useful if lineStyle was changed on graphics element) */
setLineStyle() {
const options = this.options
if (this.useTexture) {
const texture = DashLine.getTexture(options, this.dashSize)
this.graphics.lineTextureStyle({
width: options.width * options.scale,
color: options.color,
alpha: options.alpha,
texture,
alignment: options.alignment,
})
this.activeTexture = texture
} else {
this.graphics.lineStyle({
width: options.width * options.scale,
color: options.color,
alpha: options.alpha,
cap: options.cap,
join: options.join,
alignment: options.alignment,
})
}
this.scale = options.scale
}
private static distance(x1: number, y1: number, x2: number, y2: number): number {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
}
moveTo(x: number, y: number): this {
this.lineLength = 0
this.cursor.set(x, y)
this.start = new PIXI.Point(x, y)
this.graphics.moveTo(this.cursor.x, this.cursor.y)
return this
}
lineTo(x: number, y: number, closePath?: boolean): this {
if (typeof this.lineLength === undefined) {
this.moveTo(0, 0)
}
const length = DashLine.distance(this.cursor.x, this.cursor.y, x, y)
const angle = Math.atan2(y - this.cursor.y, x - this.cursor.x)
const closed = closePath && x === this.start.x && y === this.start.y
if (this.useTexture) {
this.graphics.moveTo(this.cursor.x, this.cursor.y)
this.adjustLineStyle(angle)
if (closed && this.dash.length % 2 === 0) {
const gap = Math.min(this.dash[this.dash.length - 1], length);
this.graphics.lineTo(x - Math.cos(angle) * gap, y - Math.sin(angle) * gap)
this.graphics.closePath();
} else {
this.graphics.lineTo(x, y)
}
} else {
const cos = Math.cos(angle)
const sin = Math.sin(angle)
let x0 = this.cursor.x
let y0 = this.cursor.y
// find the first part of the dash for this line
const place = this.lineLength % (this.dashSize * this.scale)
let dashIndex: number = 0, dashStart: number = 0
let dashX = 0
for (let i = 0; i < this.dash.length; i++) {
const dashSize = this.dash[i] * this.scale
if (place < dashX + dashSize) {
dashIndex = i
dashStart = place - dashX
break
} else {
dashX += dashSize
}
}
let remaining = length
// let count = 0
while (remaining > 0) { // && count++ < 1000) {
const dashSize = this.dash[dashIndex] * this.scale - dashStart
let dist = remaining > dashSize ? dashSize : remaining
if (closed) {
const remainingDistance = DashLine.distance(x0 + cos * dist, y0 + sin * dist, this.start.x, this.start.y)
if (remainingDistance <= dist) {
if (dashIndex % 2 === 0) {
const lastDash = DashLine.distance(x0, y0, this.start.x, this.start.y) - this.dash[this.dash.length - 1] * this.scale
x0 += cos * lastDash
y0 += sin * lastDash
this.graphics.lineTo(x0, y0)
}
break
}
}
x0 += cos * dist
y0 += sin * dist
if (dashIndex % 2) {
this.graphics.moveTo(x0, y0)
} else {
this.graphics.lineTo(x0, y0)
}
remaining -= dist
dashIndex++
dashIndex = dashIndex === this.dash.length ? 0 : dashIndex
dashStart = 0
}
// if (count >= 1000) console.log('failure', this.scale)
}
this.lineLength += length
this.cursor.set(x, y)
return this
}
closePath() {
this.lineTo(this.start.x, this.start.y, true)
}
drawCircle(x: number, y: number, radius: number, points = 80, matrix?: PIXI.Matrix): this {
const interval = Math.PI * 2 / points
let angle = 0, first: PIXI.Point
if (matrix) {
first = new PIXI.Point(x + Math.cos(angle) * radius, y + Math.sin(angle) * radius)
matrix.apply(first, first)
this.moveTo(first[0], first[1])
} else {
first = new PIXI.Point(x + Math.cos(angle) * radius, y + Math.sin(angle) * radius)
this.moveTo(first.x, first.y)
}
angle += interval
for (let i = 1; i < points + 1; i++) {
const next = i === points ? first : [x + Math.cos(angle) * radius, y + Math.sin(angle) * radius]
this.lineTo(next[0], next[1])
angle += interval
}
return this
}
arc(cx: number, cy: number, radius: number, startAngle: number, endAngle: number, points = 80): this {
const interval = (endAngle - startAngle) / points;
let angle = startAngle;
for (let i = 0; i < points + 1; i++) {
const next = [cx + Math.cos(angle) * radius, cy + Math.sin(angle) * radius];
if (i === 0) {
this.moveTo(next[0], next[1]);
} else {
this.lineTo(next[0], next[1]);
}
angle += interval;
}
return this;
}
drawEllipse(x: number, y: number, radiusX: number, radiusY: number, points = 80, matrix?: PIXI.Matrix): this {
const interval = Math.PI * 2 / points
let first: { x: number, y: number }
const point = new PIXI.Point()
let f = 0
for (let i = 0; i < Math.PI * 2; i += interval) {
let x0 = x - radiusX * Math.sin(i)
let y0 = y - radiusY * Math.cos(i)
if (matrix) {
point.set(x0, y0)
matrix.apply(point, point)
x0 = point.x
y0 = point.y
}
if (i === 0) {
this.moveTo(x0, y0)
first = { x: x0, y: y0 }
} else {
this.lineTo(x0, y0)
}
}
this.lineTo(first.x, first.y, true)
return this
}
drawPolygon(points: PIXI.Point[] | number[], matrix?: PIXI.Matrix): this {
const p = new PIXI.Point()
if (typeof points[0] === 'number') {
if (matrix) {
p.set(points[0] as number, points[1] as number)
matrix.apply(p, p)
this.moveTo(p.x, p.y)
for (let i = 2; i < points.length; i += 2) {
p.set(points[i] as number, points[i + 1] as number)
matrix.apply(p, p)
this.lineTo(p.x, p.y, i === points.length - 2)
}
} else {
this.moveTo(points[0] as number, points[1] as number)
for (let i = 2; i < points.length; i += 2) {
this.lineTo(points[i] as number, points[i + 1] as number, i === points.length - 2)
}
}
} else {
if (matrix) {
const point = points[0] as PIXI.Point
p.copyFrom(point)
matrix.apply(p, p)
this.moveTo(p.x, p.y)
for (let i = 1; i < points.length; i++) {
const point = points[i] as PIXI.Point
p.copyFrom(point)
matrix.apply(p, p)
this.lineTo(p.x, p.y, i === points.length - 1)
}
} else {
const point = points[0] as PIXI.Point
this.moveTo(point.x, point.y)
for (let i = 1; i < points.length; i++) {
const point = points[i] as PIXI.Point
this.lineTo(point.x, point.y, i === points.length - 1)
}
}
}
return this
}
drawRect(x: number, y: number, width: number, height: number, matrix?: PIXI.Matrix): this {
if (matrix) {
const p = new PIXI.Point()
// moveTo(x, y)
p.set(x, y)
matrix.apply(p, p)
this.moveTo(p.x, p.y)
// lineTo(x + width, y)
p.set(x + width, y)
matrix.apply(p, p)
this.lineTo(p.x, p.y)
// lineTo(x + width, y + height)
p.set(x + width, y + height)
matrix.apply(p, p)
this.lineTo(p.x, p.y)
// lineto(x, y + height)
p.set(x, y + height)
matrix.apply(p, p)
this.lineTo(p.x, p.y)
// lineTo(x, y, true)
p.set(x, y)
matrix.apply(p, p)
this.lineTo(p.x, p.y, true)
} else {
this.moveTo(x, y)
.lineTo(x + width, y)
.lineTo(x + width, y + height)
.lineTo(x, y + height)
.lineTo(x, y, true)
}
return this
}
// adjust the matrix for the dashed texture
private adjustLineStyle(angle: number) {
const lineStyle = this.graphics.line
lineStyle.matrix = new PIXI.Matrix()
if (angle) {
lineStyle.matrix.rotate(angle)
}
if (this.scale !== 1) lineStyle.matrix.scale(this.scale, this.scale)
const textureStart = -this.lineLength
lineStyle.matrix.translate(this.cursor.x + textureStart * Math.cos(angle), this.cursor.y + textureStart * Math.sin(angle))
this.graphics.lineStyle(lineStyle)
}
// creates or uses cached texture
private static getTexture(options: DashLineOptions, dashSize: number): PIXI.Texture {
const key = options.dash.toString()
if (DashLine.dashTextureCache[key]) {
return DashLine.dashTextureCache[key]
}
const canvas = document.createElement("canvas")
canvas.width = dashSize
canvas.height = Math.ceil(options.width)
const context = canvas.getContext("2d")
if (!context) {
console.warn('Did not get context from canvas')
return
}
context.strokeStyle = "white"
context.globalAlpha = options.alpha
context.lineWidth = options.width
let x = 0
const y = options.width / 2
context.moveTo(x, y)
for (let i = 0; i < options.dash.length; i += 2) {
x += options.dash[i]
context.lineTo(x, y)
if (options.dash.length !== i + 1) {
x += options.dash[i + 1]
context.moveTo(x, y)
}
}
context.stroke()
const texture = DashLine.dashTextureCache[key] = PIXI.Texture.from(canvas)
texture.baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST
return texture
}
}