import { ARROWHEAD_LENGTH, ARROWHEAD_WIDTH, } from './config.js'; export class Display { scalePower = 0; viewOrigin = {x: 0, y: 0}; constructor(sim) { const canvas = document.createElement('canvas') this.canvas = canvas; if (sim) { // Create canvas that fills the window // If the window resizes, also resize the canvas sim.div.appendChild(canvas); this.fullscreen(); window.addEventListener('resize', () => this.fullscreen()); } } frame() { // Clear canvas in preparation for other modules to render this frame this.fillCanvas(); } get scale() { return 2 ** this.scalePower; } get ctx() { const ctx = this.canvas.getContext("2d"); ctx.resetTransform(); ctx.scale(this.scale, this.scale); ctx.translate(-this.viewOrigin.x, -this.viewOrigin.y); return ctx; } get width() { return this.canvas.width / this.scale; } get height() { return this.canvas.height / this.scale; } screenToSim(x, y) { return { x: x / this.scale + this.viewOrigin.x, y: y / this.scale + this.viewOrigin.y, }; } simToScreen(x, y) { return { x: (x - this.viewOrigin.x) * this.scale, y: (y - this.viewOrigin.y) * this.scale, }; } fullscreen() { this.canvas.width = document.documentElement.clientWidth; this.canvas.height = document.documentElement.clientHeight; } fillCanvas() { const ctx = this.ctx; ctx.fillStyle = '#000'; ctx.fillRect(this.viewOrigin.x, this.viewOrigin.y, this.width, this.height); } drawArrow(startX, startY, endX, endY, { style, width, arrowhead, arrowheadLength, fill, ifShort }) { const ctx = this.ctx; ctx.strokeStyle = style; // Keep arrows at normal scale ctx.lineWidth = width / this.scale; arrowhead = arrowhead ?? true; arrowheadLength = arrowheadLength ?? ARROWHEAD_LENGTH; const scaledArrowheadLength = arrowheadLength / this.scale; ifShort = ifShort ?? 'tail'; const arrowDirection = Math.atan2(endY - startY, endX - startX); const length = Math.sqrt((endX - startX) ** 2 + (endY - startY) ** 2); let tail = true; if (!length) { return; } if (length <= scaledArrowheadLength) { switch (ifShort) { case 'head': { arrowhead = true; tail = false; break; } case 'tail': { arrowhead = false; tail = true; break; } } } if (tail) { const endAx = arrowhead ? endX - (scaledArrowheadLength) * Math.cos(arrowDirection) : endX; const endAy = arrowhead ? endY - (scaledArrowheadLength) * Math.sin(arrowDirection) : endY; ctx.beginPath(); ctx.moveTo(startX, startY); ctx.lineTo(endAx, endAy); ctx.stroke(); } if (arrowhead) { this.drawArrowHead(endX, endY, arrowDirection, {style, length: arrowheadLength, fill}); } } drawArrowHead(x, y, direction, {style, length, fill}) { const arrowheadLength = length ?? ARROWHEAD_LENGTH; const arrowheadWidth = arrowheadLength / ARROWHEAD_LENGTH * ARROWHEAD_WIDTH; const ctx = this.ctx; ctx.fillStyle = style; ctx.strokeStyle = style; // To make this simple, draw the arrowhead and then rotate and translate it as needed. // Keep arrows at normal scale const scaledArrowheadLength = arrowheadLength / this.scale; const scaledArrowheadWidth = arrowheadWidth / this.scale; ctx.beginPath(); ctx.moveTo(x, y); ctx.translate(x, y); ctx.rotate(direction); ctx.lineTo(-scaledArrowheadLength, -scaledArrowheadWidth / 2); ctx.lineTo(-scaledArrowheadLength, scaledArrowheadWidth / 2); ctx.closePath(); if (fill !== false) { ctx.fill(); } else { ctx.stroke(); } ctx.resetTransform(); } drawBox({start, end}) { const ctx = this.ctx; ctx.strokeStyle = 'rgb(0, 255, 0)'; ctx.strokeRect(start.x, start.y, end.x - start.x, end.y - start.y); } }