import { ARROWHEAD_LENGTH, ARROWHEAD_WIDTH, } from './config.js'; export class Display { sim = undefined; scalePower = 0; viewOrigin = {x: 0, y: 0}; constructor(sim) { this.sim = sim; // Create canvas that fills the window // If the window resizes, also resize the canvas const canvas = document.createElement('canvas') this.canvas = canvas; this.sim.div.appendChild(canvas); canvas.style.position = 'absolute'; canvas.style.top = 0; canvas.style.left = 0; this.fullscreen(); window.addEventListener('resize', () => this.fullscreen()); } 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; if (this.sim.getOption('debug.canvasSize')) { this.sim.info['Canvas'] = [`${this.canvas.width}`, `${this.canvas.height}`]; } } fillCanvas() { const ctx = this.ctx; ctx.fillStyle = '#000'; ctx.fillRect(this.viewOrigin.x, this.viewOrigin.y, this.width, this.height); } drawObjects() { this.sim.objects.forEachObject(obj => obj.drawObject(this.sim), {alive: null}); } 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(); } computePanning(elapsedTime) { // Add another entry for the current pointer position const { pointerHistory, panTouchStart: start, panTouchLatest: latest, } = this.sim.pointer ?? {}; if (pointerHistory?.length) { const currentPointer = pointerHistory[pointerHistory.length - 1]; this.sim.pointer.updatePointer(currentPointer); } if (start && latest) { // Direct translate this.viewOrigin.x = start.viewOrigin.x - (latest.x - start.x) / this.scale; this.viewOrigin.y = start.viewOrigin.y - (latest.y - start.y) / this.scale; } else if (this.sim.panning && !this.sim.panning.paused) { // Apply update to viewOrigin based on panning const { velocity } = this.sim.panning; this.viewOrigin.x -= velocity.x * elapsedTime; this.viewOrigin.y -= velocity.y * elapsedTime; } } }