UNPKG

pixi-dashed-line2

Version:

A pixi.js implementation to support dashed lines in PIXI.Graphics.

383 lines (348 loc) 13.8 kB
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 } }