UNPKG

transitive-js

Version:

A tool for generating dynamic stylized transit maps that are easy to understand.

164 lines (133 loc) 4.45 kB
import roundedRect from 'rounded-rect' import Display from './display' export default class CanvasDisplay extends Display { constructor(transitive) { super(transitive) const { canvas, el } = transitive.options // Handle case of externally-provided canvas if (canvas) { // Set internal dimensions to match those of canvas this.setDimensions(canvas.width, canvas.height) this.setCanvas(canvas) // We have a DOM element; create canvas } else if (el) { this.setDimensions(el.clientWidth, el.clientHeight) const canvas = document.createElement('canvas') canvas.width = el.clientWidth canvas.height = el.clientHeight el.appendChild(canvas) // Check for Hi-PPI display if (window.devicePixelRatio > 1) makeCanvasHiPPI(canvas) this.setCanvas(canvas) } } setCanvas(canvas) { this.canvas = canvas this.ctx = this.canvas.getContext('2d') } clear() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) } drawRect(upperLeft, attrs) { this.ctx.strokeStyle = attrs.stroke this.ctx.lineWidth = attrs['stroke-width'] this.ctx.fillStyle = attrs.fill this.ctx.beginPath() if (attrs.rx && attrs.ry && attrs.rx === attrs.ry) { roundedRect( this.ctx, upperLeft.x, upperLeft.y, attrs.width, attrs.height, attrs.rx ) // TODO: handle case where rx != ry } else { // ordinary rectangle this.ctx.rect(upperLeft.x, upperLeft.y, attrs.width, attrs.height) } this.ctx.closePath() if (attrs.fill) this.ctx.fill() if (attrs.stroke) this.ctx.stroke() } drawCircle(center, attrs) { this.ctx.beginPath() this.ctx.arc(center.x, center.y, attrs.r, 0, Math.PI * 2, true) this.ctx.closePath() if (attrs.fill) { this.ctx.fillStyle = attrs.fill this.ctx.fill() } if (attrs.stroke) { this.ctx.strokeStyle = attrs.stroke this.ctx.lineWidth = attrs['stroke-width'] || 1 this.ctx.stroke() } } drawEllipse(center, attrs) { // TODO: implement } drawPath(renderData, attrs) { if (!renderData || renderData.length === 0) return // Apply stroke/width styles this.ctx.strokeStyle = attrs.stroke this.ctx.lineWidth = attrs['stroke-width'] // Apply line-dash style, if present if (attrs['stroke-dasharray']) { const arr = attrs['stroke-dasharray'] .split(',') .map((str) => parseFloat(str.trim())) this.ctx.setLineDash(arr) } // Apply linecap style this.ctx.lineCap = attrs['stroke-linecap'] || 'butt' // Apply 'round' line join for smooth edge rendering. // (The default join is 'miter', which causes jagged edges when zoomed away.) this.ctx.lineJoin = 'round' // Draw the path this.ctx.beginPath() this.ctx.moveTo(renderData[0].x, renderData[0].y) for (let i = 1; i < renderData.length; i++) { const rd = renderData[i] if (rd.arc) this.ctx.arcTo(rd.ex, rd.ey, rd.x, rd.y, rd.radius) else this.ctx.lineTo(rd.x, rd.y) } this.ctx.stroke() // Revert line-dash style to default if (attrs['stroke-dasharray']) this.ctx.setLineDash([]) } drawText(text, anchor, attrs) { // For equivalence w/ SVG text rendering this.ctx.textBaseline = 'top' this.ctx.font = `${attrs['font-size'] || '14px'} ${ attrs['font-family'] || 'sans-serif' }` if (attrs['text-anchor']) this.ctx.textAlign = attrs['text-anchor'] if (attrs.stroke) { this.ctx.strokeStyle = attrs.stroke if (attrs['stroke-opacity']) { this.ctx.globalAlpha = attrs['stroke-opacity'] } this.ctx.lineWidth = attrs['stroke-width'] || 1 this.ctx.strokeText(text, anchor.x, anchor.y) } if (attrs.fill) { this.ctx.fillStyle = attrs.fill if (attrs['fill-opacity']) this.ctx.globalAlpha = attrs['fill-opacity'] this.ctx.fillText(text, anchor.x, anchor.y) } this.ctx.textAlign = 'start' this.ctx.globalAlpha = 1 } } // Utility function to support HiPPI displays (e.g. Retina) function makeCanvasHiPPI(canvas) { const PIXEL_RATIO = 2 canvas.style.width = canvas.width + 'px' canvas.style.height = canvas.height + 'px' canvas.width *= PIXEL_RATIO canvas.height *= PIXEL_RATIO const context = canvas.getContext('2d') context.scale(PIXEL_RATIO, PIXEL_RATIO) }