gravity/display.js
2025-12-29 15:18:37 -06:00

170 lines
4.8 KiB
JavaScript

import {
ARROWHEAD_LENGTH,
ARROWHEAD_WIDTH,
DISPLAY_CANVAS_SIZE,
} 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 (DISPLAY_CANVAS_SIZE) {
this.sim.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);
}
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;
}
}
}