import { VELOCITY_VECTOR_SCALE, VELOCITY_VECTOR_COLOR, VELOCITY_VECTOR_WIDTH, VELOCITY_VECTOR_ARROWHEAD, ARROWHEAD_LENGTH, ARROWHEAD_WIDTH, OFFSCREEN_OBJECT_LINE_SCALE, OFFSCREEN_OBJECT_LINE_WIDTH, OFFSCREEN_OBJECT_ARROWHEAD_LENGTH, } from './config.js'; export class Display { sim = undefined; scale = 1; 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 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, }; } fullscreen() { this.canvas.width = document.documentElement.clientWidth; this.canvas.height = document.documentElement.clientHeight; // this.info['Canvas'] = `${this.canvas.width} x ${this.canvas.height}`; } fillCanvas() { const ctx = this.ctx; ctx.fillStyle = '#000'; ctx.fillRect(this.viewOrigin.x, this.viewOrigin.y, this.width, this.height); } drawObject(idx) { const obj = this.sim.objects.object(idx); const ctx = this.ctx; const {r, g, b} = obj.color; const {x, y} = obj.position; const {x: vx, y: vy} = obj.velocity; const radius = obj.radius; const {height: H, width: W} = this; const ox = this.viewOrigin.x; const oy = this.viewOrigin.y; const cx = ox + W / 2; const cy = oy + H / 2; // If the object is outside the display area, draw an arrow at the edge of the display if (Math.abs(x - cx) - radius >= W / 2 || Math.abs(y - cy) - radius >= H / 2) { // Find where a line from center of display to object intersects display edge let px, py; if (y <= cy) { // Line intersects y = 0: const y0px = cx + (H / 2) / (cy - y) * (x - cx); if (Math.abs(y0px - cx) <= W / 2) { px = y0px; py = this.viewOrigin.y; } } else { // Line intersects y = H const yHpx = cx + (H / 2) / (y - cy) * (x - cx); if (Math.abs(yHpx - cx) <= W / 2) { px = yHpx; py = this.viewOrigin.y + H; } } if (px === undefined) { if (x <= cx) { // Line intersects x = 0: px = this.viewOrigin.x; py = cy + (W / 2) / (cx - x) * (y - cy); } else { // Line intersects x = W: px = this.viewOrigin.x + W; py = cy + (W / 2) / (x - cx) * (y - cy); } } const arrowDirection = Math.atan2(py - cy, px - cx); // Length of arrow based on distance (logarithmic scale) const distance = Math.sqrt((x - px)**2, (y - py)**2); const arrowLength = Math.log(distance) * OFFSCREEN_OBJECT_LINE_SCALE / this.scale; const startAx = px - arrowLength * Math.cos(arrowDirection); const startAy = py - arrowLength * Math.sin(arrowDirection); this.drawArrow(startAx, startAy, px, py, { style: `rgb(${r}, ${g}, ${b})`, width: OFFSCREEN_OBJECT_LINE_WIDTH, arrowheadLength: OFFSCREEN_OBJECT_ARROWHEAD_LENGTH }); return; } // Draw filled circle for the object ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; ctx.beginPath(); ctx.arc(x, y, radius, 0, 2*Math.PI); ctx.fill(); // Draw arrow for the velocity const endVx = x + VELOCITY_VECTOR_SCALE * vx; const endVy = y + VELOCITY_VECTOR_SCALE * vy; const style = VELOCITY_VECTOR_COLOR === 'object color' ? ctx.fillStyle : VELOCITY_VECTOR_COLOR; this.drawArrow(x, y, endVx, endVy, { style, width: VELOCITY_VECTOR_WIDTH, arrowhead: VELOCITY_VECTOR_ARROWHEAD }); // TODO: Draw arrow for acceleration } drawObjects() { for (let i = 0; i < this.sim.objects.length; i++) { this.drawObject(i); } } drawArrow(startX, startY, endX, endY, {style, width, arrowhead, arrowheadLength}) { arrowhead = arrowhead ?? true; arrowheadLength = arrowheadLength ?? ARROWHEAD_LENGTH; const ctx = this.ctx; ctx.strokeStyle = style; // Keep arrows at normal scale ctx.lineWidth = width / this.scale; const scaledArrowheadLength = arrowheadLength / this.scale; const arrowDirection = Math.atan2(endY - startY, endX - startX); const endAx = arrowhead ? endX - (scaledArrowheadLength - 1) * Math.cos(arrowDirection) : endX; const endAy = arrowhead ? endY - (scaledArrowheadLength - 1) * 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}); } } drawArrowHead(x, y, direction, {style, length}) { const arrowheadLength = length ?? ARROWHEAD_LENGTH; const arrowheadWidth = arrowheadLength / ARROWHEAD_LENGTH * ARROWHEAD_WIDTH; const ctx = this.ctx; ctx.fillStyle = 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(); ctx.fill(); ctx.resetTransform(); } }