lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
| 1 | import type { Surface, TextStyle } from './surface'; |
| 2 | |
| 3 | /** Zeichenflaeche auf einem HTML-Canvas. */ |
| 4 | export class CanvasSurface implements Surface { |
| 5 | readonly width: number; |
| 6 | readonly height: number; |
| 7 | |
| 8 | constructor( |
| 9 | private readonly ctx: CanvasRenderingContext2D, |
| 10 | width: number, |
| 11 | height: number, |
| 12 | private readonly fontFamily = 'system-ui, "Segoe UI", Roboto, Arial, sans-serif', |
| 13 | ) { |
| 14 | this.width = width; |
| 15 | this.height = height; |
| 16 | } |
| 17 | |
| 18 | /** |
| 19 | * Legt einen Canvas in der Geraeteaufloesung an, damit die Darstellung auf |
| 20 | * hochaufloesenden Bildschirmen scharf bleibt. Der Altbestand zeichnete in |
| 21 | * CSS-Pixeln; Beschriftungen waren dadurch unscharf. |
| 22 | */ |
| 23 | static create(canvas: HTMLCanvasElement, width: number, height: number): CanvasSurface | null { |
| 24 | const ratio = Math.min(3, Math.max(1, window.devicePixelRatio || 1)); |
| 25 | canvas.width = Math.round(width * ratio); |
| 26 | canvas.height = Math.round(height * ratio); |
| 27 | canvas.style.width = `${width}px`; |
| 28 | canvas.style.height = `${height}px`; |
| 29 | |
| 30 | const ctx = canvas.getContext('2d'); |
| 31 | if (!ctx) return null; |
| 32 | ctx.setTransform(ratio, 0, 0, ratio, 0, 0); |
| 33 | ctx.clearRect(0, 0, width, height); |
| 34 | return new CanvasSurface(ctx, width, height); |
| 35 | } |
| 36 | |
| 37 | rect(x: number, y: number, width: number, height: number, fill: string): void { |
| 38 | this.ctx.fillStyle = fill; |
| 39 | this.ctx.fillRect(x, y, width, height); |
| 40 | } |
| 41 | |
| 42 | polygon(points: readonly { readonly x: number; readonly y: number }[], fill: string): void { |
| 43 | if (points.length < 3) return; |
| 44 | const [erster, ...weitere] = points; |
| 45 | if (erster === undefined) return; |
| 46 | this.ctx.beginPath(); |
| 47 | this.ctx.moveTo(erster.x, erster.y); |
| 48 | for (const punkt of weitere) this.ctx.lineTo(punkt.x, punkt.y); |
| 49 | this.ctx.closePath(); |
| 50 | this.ctx.fillStyle = fill; |
| 51 | this.ctx.fill(); |
| 52 | } |
| 53 | |
| 54 | strokeRect( |
| 55 | x: number, |
| 56 | y: number, |
| 57 | width: number, |
| 58 | height: number, |
| 59 | color: string, |
| 60 | lineWidth = 1, |
| 61 | ): void { |
| 62 | this.ctx.strokeStyle = color; |
| 63 | this.ctx.lineWidth = lineWidth; |
| 64 | // Der halbe Pixel-Versatz haelt duenne Linien scharf. |
| 65 | const offset = lineWidth % 2 === 1 ? 0.5 : 0; |
| 66 | this.ctx.strokeRect(x + offset, y + offset, width, height); |
| 67 | } |
| 68 | |
| 69 | line(x1: number, y1: number, x2: number, y2: number, color: string, lineWidth = 1): void { |
| 70 | this.ctx.save(); |
| 71 | this.ctx.setLineDash([]); |
| 72 | this.drawLine(x1, y1, x2, y2, color, lineWidth); |
| 73 | this.ctx.restore(); |
| 74 | } |
| 75 | |
| 76 | circle(x: number, y: number, radius: number, fill: string, stroke: string, lineWidth = 1): void { |
| 77 | if (radius <= 0) return; |
| 78 | this.ctx.save(); |
| 79 | this.ctx.setLineDash([]); |
| 80 | this.ctx.beginPath(); |
| 81 | this.ctx.arc(x, y, radius, 0, Math.PI * 2); |
| 82 | if (fill !== '') { |
| 83 | this.ctx.fillStyle = fill; |
| 84 | this.ctx.fill(); |
| 85 | } |
| 86 | if (stroke !== '') { |
| 87 | this.ctx.strokeStyle = stroke; |
| 88 | this.ctx.lineWidth = lineWidth; |
| 89 | this.ctx.stroke(); |
| 90 | } |
| 91 | this.ctx.restore(); |
| 92 | } |
| 93 | |
| 94 | dashedLine(x1: number, y1: number, x2: number, y2: number, color: string, lineWidth = 1): void { |
| 95 | this.ctx.save(); |
| 96 | this.ctx.setLineDash([3, 3]); |
| 97 | this.drawLine(x1, y1, x2, y2, color, lineWidth); |
| 98 | this.ctx.restore(); |
| 99 | } |
| 100 | |
| 101 | private drawLine( |
| 102 | x1: number, |
| 103 | y1: number, |
| 104 | x2: number, |
| 105 | y2: number, |
| 106 | color: string, |
| 107 | lineWidth: number, |
| 108 | ): void { |
| 109 | const offset = lineWidth % 2 === 1 ? 0.5 : 0; |
| 110 | this.ctx.strokeStyle = color; |
| 111 | this.ctx.lineWidth = lineWidth; |
| 112 | this.ctx.beginPath(); |
| 113 | this.ctx.moveTo(x1 + offset, y1 + offset); |
| 114 | this.ctx.lineTo(x2 + offset, y2 + offset); |
| 115 | this.ctx.stroke(); |
| 116 | } |
| 117 | |
| 118 | text(value: string, x: number, y: number, style: TextStyle): void { |
| 119 | this.ctx.font = this.font(style); |
| 120 | this.ctx.fillStyle = style.color ?? '#000000'; |
| 121 | this.ctx.textAlign = |
| 122 | style.align === 'mitte' ? 'center' : style.align === 'rechts' ? 'right' : 'left'; |
| 123 | this.ctx.textBaseline = |
| 124 | style.baseline === 'mitte' ? 'middle' : style.baseline === 'unten' ? 'alphabetic' : 'top'; |
| 125 | this.ctx.fillText(value, x, y); |
| 126 | } |
| 127 | |
| 128 | measureText(value: string, style: TextStyle): number { |
| 129 | this.ctx.font = this.font(style); |
| 130 | return this.ctx.measureText(value).width; |
| 131 | } |
| 132 | |
| 133 | private font(style: TextStyle): string { |
| 134 | return `${style.bold === true ? '600 ' : ''}${style.size}px ${this.fontFamily}`; |
| 135 | } |
| 136 | } |