gravity/display.js
2025-12-25 12:15:08 -06:00

191 lines
6.0 KiB
JavaScript

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();
}
}