289 lines
8.8 KiB
JavaScript
289 lines
8.8 KiB
JavaScript
import {
|
|
VELOCITY_VECTOR_SCALE,
|
|
VELOCITY_VECTOR_COLOR,
|
|
VELOCITY_VECTOR_WIDTH,
|
|
VELOCITY_VECTOR_ARROWHEAD,
|
|
ACCELERATION_VECTOR_SCALE,
|
|
ACCELERATION_VECTOR_COLOR,
|
|
ACCELERATION_VECTOR_WIDTH,
|
|
ACCELERATION_VECTOR_ARROWHEAD,
|
|
ARROWHEAD_LENGTH,
|
|
ARROWHEAD_WIDTH,
|
|
OFFSCREEN_OBJECT_LINE_SCALE,
|
|
OFFSCREEN_OBJECT_LINE_WIDTH,
|
|
OFFSCREEN_OBJECT_ARROWHEAD_LENGTH,
|
|
DISPLAY_CANVAS_SIZE,
|
|
PATH_TRACES_COLOR,
|
|
PATH_TRACES_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 (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);
|
|
}
|
|
|
|
drawObject(obj) {
|
|
const ctx = this.ctx;
|
|
const {r, g, b} = obj.color;
|
|
const {x, y} = obj.position;
|
|
const {x: vx, y: vy} = obj.velocity;
|
|
const acceleration = obj.acceleration;
|
|
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;
|
|
|
|
// Draw path traces
|
|
if (this.sim.getOption('display.traces') && obj.history?.length) {
|
|
ctx.strokeStyle = PATH_TRACES_COLOR === 'object color' ?
|
|
`rgb(${r}, ${g}, ${b})` : PATH_TRACES_COLOR;
|
|
ctx.lineWidth = PATH_TRACES_WIDTH / this.scale;
|
|
ctx.beginPath();
|
|
let dash = false;
|
|
for (let i = 0; i < obj.history.length; i++) {
|
|
// for (let i = obj.history.length - 1; i >= 0; i--) {
|
|
if (i % 2) continue;
|
|
const {position} = obj.history[i];
|
|
const x = position.x;
|
|
const y = position.y;
|
|
if (dash) {
|
|
ctx.lineTo(x, y);
|
|
dash = false;
|
|
} else if (Math.abs(x - cx) <= W / 2 &&
|
|
Math.abs(y - cy) <= H / 2) {
|
|
ctx.moveTo(x, y);
|
|
dash = true;
|
|
}
|
|
}
|
|
ctx.stroke();
|
|
}
|
|
|
|
if (!obj.alive) return;
|
|
|
|
// 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) * this.scale;
|
|
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,
|
|
fill: false,
|
|
ifShort: 'head',
|
|
});
|
|
|
|
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
|
|
if (this.sim.getOption('display.velocity')) {
|
|
const speed = Math.sqrt(vx ** 2 + vy ** 2);
|
|
const endVx = x + VELOCITY_VECTOR_SCALE * vx / speed * Math.log(speed);
|
|
const endVy = y + VELOCITY_VECTOR_SCALE * vy / speed * Math.log(speed);
|
|
const style = VELOCITY_VECTOR_COLOR === 'object color' ?
|
|
`rgb(${r}, ${g}, ${b})` : VELOCITY_VECTOR_COLOR;
|
|
this.drawArrow(x, y, endVx, endVy, {
|
|
style,
|
|
width: VELOCITY_VECTOR_WIDTH,
|
|
arrowhead: VELOCITY_VECTOR_ARROWHEAD,
|
|
fill: false,
|
|
ifShort: 'head'
|
|
});
|
|
}
|
|
|
|
// Draw arrow for acceleration
|
|
if (this.sim.getOption('display.acceleration')) {
|
|
const accelerationMagnitude = Math.sqrt(acceleration.x ** 2 + acceleration.y ** 2);
|
|
const endAx = x + ACCELERATION_VECTOR_SCALE * acceleration.x /
|
|
accelerationMagnitude * Math.log(accelerationMagnitude);
|
|
const endAy = y + ACCELERATION_VECTOR_SCALE * acceleration.y /
|
|
accelerationMagnitude * Math.log(accelerationMagnitude);
|
|
const style = ACCELERATION_VECTOR_COLOR === 'object color' ?
|
|
`rgb(${r}, ${g}, ${b})` : ACCELERATION_VECTOR_COLOR;
|
|
this.drawArrow(x, y, endAx, endAy, {
|
|
style,
|
|
width: ACCELERATION_VECTOR_WIDTH,
|
|
arrowhead: ACCELERATION_VECTOR_ARROWHEAD,
|
|
fill: false,
|
|
ifShort: 'tail'
|
|
});
|
|
}
|
|
}
|
|
|
|
drawObjects() {
|
|
this.sim.objects.forEachObject(obj => this.drawObject(obj), 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();
|
|
}
|
|
}
|