Compare commits

...

2 Commits

Author SHA1 Message Date
Ladd
56414a7ca1 Implemented "Clear Traces" tool 2025-12-29 02:09:22 -06:00
Ladd
dc4d92c9cd refactor, moved drawObject to Object class 2025-12-29 02:00:43 -06:00
5 changed files with 199 additions and 170 deletions

View File

@ -19,4 +19,9 @@ TODO
- [ ] Zoom Easing - [ ] Zoom Easing
- [ ] 2-touch Pan & Zoom - [ ] 2-touch Pan & Zoom
- [ ] Multi-touch Mass Create - [ ] Multi-touch Mass Create
- [ ] Tool to Clear Traces - [x] Tool to Clear Traces
- [ ] Undo feature:
- [ ] Undo "Clear Traces" Action
- [ ] Undo "Reset
- [ ] Save to LocalStorage
- [ ] Lossy Rescaling To Widen Zoom (Handling overflow/underflow)

View File

@ -9,13 +9,13 @@ export const DISPLAY_ACCELERATION_VECTORS = true;
export const DISPLAY_PATH_TRACES = false; export const DISPLAY_PATH_TRACES = false;
// VELOCITY // VELOCITY
export const VELOCITY_VECTOR_SCALE = 5E0; export const VELOCITY_VECTOR_SCALE = 8E0;
export const VELOCITY_VECTOR_COLOR = 'rgba(150, 150, 150, 0.8)'; // optionally set to 'object color' export const VELOCITY_VECTOR_COLOR = 'rgba(150, 150, 150, 0.8)'; // optionally set to 'object color'
export const VELOCITY_VECTOR_WIDTH = 1.5; export const VELOCITY_VECTOR_WIDTH = 1.5;
export const VELOCITY_VECTOR_ARROWHEAD = true; export const VELOCITY_VECTOR_ARROWHEAD = true;
// ACCELERATION // ACCELERATION
export const ACCELERATION_VECTOR_SCALE = 5E0; export const ACCELERATION_VECTOR_SCALE = 8E0;
export const ACCELERATION_VECTOR_COLOR = 'rgba(0, 128, 0, 0.8)'; // optionally set to 'object color' export const ACCELERATION_VECTOR_COLOR = 'rgba(0, 128, 0, 0.8)'; // optionally set to 'object color'
export const ACCELERATION_VECTOR_WIDTH = 1.5; export const ACCELERATION_VECTOR_WIDTH = 1.5;
export const ACCELERATION_VECTOR_ARROWHEAD = true; export const ACCELERATION_VECTOR_ARROWHEAD = true;

View File

@ -1,22 +1,7 @@
import { import {
ACCELERATION_VECTOR_ARROWHEAD,
ACCELERATION_VECTOR_COLOR,
ACCELERATION_VECTOR_SCALE,
ACCELERATION_VECTOR_WIDTH,
ARROWHEAD_LENGTH, ARROWHEAD_LENGTH,
ARROWHEAD_WIDTH, ARROWHEAD_WIDTH,
DISPLAY_CANVAS_SIZE, DISPLAY_CANVAS_SIZE,
OFFSCREEN_OBJECT_ARROWHEAD_LENGTH,
OFFSCREEN_OBJECT_LINE_SCALE,
OFFSCREEN_OBJECT_LINE_WIDTH,
PATH_TRACES_COLOR,
PATH_TRACES_OPACITY,
PATH_TRACES_DASHED_OPACITY,
PATH_TRACES_WIDTH,
VELOCITY_VECTOR_ARROWHEAD,
VELOCITY_VECTOR_COLOR,
VELOCITY_VECTOR_SCALE,
VELOCITY_VECTOR_WIDTH,
} from './config.js'; } from './config.js';
export class Display { export class Display {
@ -86,139 +71,8 @@ export class Display {
ctx.fillRect(this.viewOrigin.x, this.viewOrigin.y, this.width, this.height); 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) {
const dashedTraces = this.sim.getOption('display.dashedTraces');
const opacity = dashedTraces ? PATH_TRACES_DASHED_OPACITY : PATH_TRACES_OPACITY;
ctx.strokeStyle = PATH_TRACES_COLOR === 'object color' ?
`rgba(${r}, ${g}, ${b}, ${opacity})` : PATH_TRACES_COLOR;
ctx.lineWidth = PATH_TRACES_WIDTH / this.scale;
ctx.beginPath();
let dash = false;
for (let i = 0; i < obj.history.length ; i++) {
// if (i % 2 > 0) continue;
const {position: {x, y}} = obj.history[i];
if (dashedTraces) {
if (dash) {
ctx.lineTo(x, y);
} else {
ctx.moveTo(x, y);
}
dash = !dash;
} else {
ctx.lineTo(x, y);
}
}
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() { drawObjects() {
this.sim.objects.forEachObject(obj => this.drawObject(obj), null); this.sim.objects.forEachObject(obj => obj.drawObject(this.sim), null);
} }
drawArrow(startX, startY, endX, endY, {style, width, arrowhead, arrowheadLength, fill, ifShort}) { drawArrow(startX, startY, endX, endY, {style, width, arrowhead, arrowheadLength, fill, ifShort}) {

159
object.js
View File

@ -1,3 +1,21 @@
import {
ACCELERATION_VECTOR_ARROWHEAD,
ACCELERATION_VECTOR_COLOR,
ACCELERATION_VECTOR_SCALE,
ACCELERATION_VECTOR_WIDTH,
OFFSCREEN_OBJECT_ARROWHEAD_LENGTH,
OFFSCREEN_OBJECT_LINE_SCALE,
OFFSCREEN_OBJECT_LINE_WIDTH,
PATH_TRACES_COLOR,
PATH_TRACES_DASHED_OPACITY,
PATH_TRACES_OPACITY,
PATH_TRACES_WIDTH,
VELOCITY_VECTOR_ARROWHEAD,
VELOCITY_VECTOR_COLOR,
VELOCITY_VECTOR_SCALE,
VELOCITY_VECTOR_WIDTH,
} from './config.js';
export class MassObject { export class MassObject {
mass = 0; mass = 0;
density = 1; density = 1;
@ -40,5 +58,146 @@ export class MassObject {
y: ay / this.mass, y: ay / this.mass,
}; };
} }
drawObject(sim) {
const {
color: {r, g, b},
position: {x, y},
velocity: {x: vx, y: vy},
acceleration,
radius,
} = this;
const {
display: {
ctx,
height: H,
width: W,
viewOrigin: {x: ox, y: oy},
}
} = sim;
const cx = ox + W / 2;
const cy = oy + H / 2;
// Draw path traces
if (sim.getOption('display.traces') && this.history?.length) {
const dashedTraces = sim.getOption('display.dashedTraces');
const opacity = dashedTraces ? PATH_TRACES_DASHED_OPACITY : PATH_TRACES_OPACITY;
ctx.strokeStyle = PATH_TRACES_COLOR === 'object color' ?
`rgba(${r}, ${g}, ${b}, ${opacity})` : PATH_TRACES_COLOR;
ctx.lineWidth = PATH_TRACES_WIDTH / this.scale;
ctx.beginPath();
let dash = false;
for (let i = 0; i < this.history.length; i++) {
// if (i % 2 > 0) continue;
const {position: {x, y}} = this.history[i];
if (dashedTraces) {
if (dash) {
ctx.lineTo(x, y);
} else {
ctx.moveTo(x, y);
}
dash = !dash;
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
}
if (!this.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 = oy;
}
} else {
// Line intersects y = H
const yHpx = cx + (H / 2) / (y - cy) * (x - cx);
if (Math.abs(yHpx - cx) <= W / 2) {
px = yHpx;
py = oy + H;
}
}
if (px === undefined) {
if (x <= cx) {
// Line intersects x = 0:
px = ox;
py = cy + (W / 2) / (cx - x) * (y - cy);
} else {
// Line intersects x = W:
px = ox + 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);
sim.display.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 (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;
sim.display.drawArrow(x, y, endVx, endVy, {
style,
width: VELOCITY_VECTOR_WIDTH,
arrowhead: VELOCITY_VECTOR_ARROWHEAD,
fill: false,
ifShort: 'head'
});
}
// Draw arrow for acceleration
if (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;
sim.display.drawArrow(x, y, endAx, endAy, {
style,
width: ACCELERATION_VECTOR_WIDTH,
arrowhead: ACCELERATION_VECTOR_ARROWHEAD,
fill: false,
ifShort: 'tail'
});
}
}
} }

View File

@ -4,7 +4,11 @@ import {Tool} from '../tool.js';
export class PlayPause extends Tool { export class PlayPause extends Tool {
playHTML = 'Play'; playHTML = 'Play';
pauseHTML = 'Pause'; pauseHTML = 'Pause';
clearTracesText = 'Clear Traces';
currentTimeEl = undefined; currentTimeEl = undefined;
pauseButton = undefined;
playButton = undefined;
clearTracesEl = undefined;
get timeText() { get timeText() {
let time = this.sim.time; let time = this.sim.time;
@ -36,52 +40,59 @@ export class PlayPause extends Tool {
} }
} }
updateButtons() {
this.pauseButton.style.opacity = this.sim.playing ? '100%' : '50%';
this.playButton.style.opacity = this.sim.playing ? '50%' : '100%';
}
constructor(toolbar) { constructor(toolbar) {
super(toolbar); super(toolbar);
const currentTime = document.createElement('button'); const currentTime = document.createElement('button');
const pauseButton = document.createElement('button'); const pauseButton = document.createElement('button');
const playButton = document.createElement('button'); const playButton = document.createElement('button');
const clearTraces = document.createElement('button');
this.pauseButton = pauseButton;
this.playButton = playButton;
this.clearTracesEl = clearTraces;
this.currentTimeEl = currentTime; this.currentTimeEl = currentTime;
this.div.appendChild(currentTime); this.div.appendChild(currentTime);
this.div.appendChild(pauseButton); this.div.appendChild(pauseButton);
this.div.appendChild(playButton); this.div.appendChild(playButton);
this.div.appendChild(clearTraces);
currentTime.classList.add(TOOL_INFO_CLASSNAME); currentTime.classList.add(TOOL_INFO_CLASSNAME);
currentTime.classList.add(WIDE_CLASSNAME); currentTime.classList.add(WIDE_CLASSNAME);
clearTraces.classList.add(WIDE_CLASSNAME);
pauseButton.innerHTML = this.pauseHTML; pauseButton.innerHTML = this.pauseHTML;
playButton.innerHTML = this.playHTML; playButton.innerHTML = this.playHTML;
currentTime.innerHTML = this.timeText; currentTime.innerHTML = this.timeText;
clearTraces.innerHTML = this.clearTracesText;
pauseButton.style.opacity = this.sim.playing ? '100%' : '50%'; this.updateButtons();
playButton.style.opacity = this.sim.playing ? '50%' : '100%';
pauseButton.addEventListener('click', (e) => { pauseButton.addEventListener('click', () => {
e.stopPropagation();
this.sim.pointer.panning = undefined; this.sim.pointer.panning = undefined;
if (this.playing) { if (this.sim.playing) {
this.playing = false; this.sim.playing = false;
pauseButton.style.opacity = '50%'; this.updateButtons();
playButton.style.opacity = '100%';
} }
}); });
playButton.addEventListener('click', () => { playButton.addEventListener('click', () => {
if (!this.playing) { if (!this.sim.playing) {
this.playing = true; this.sim.playing = true;
pauseButton.style.opacity = '100%'; this.updateButtons();
playButton.style.opacity = '50%';
} }
}); });
}
get playing() { clearTraces.addEventListener('click', () => {
return this.sim.playing; // Obliterate object histories
} this.sim.objects.forEachObject(obj => {
obj.history = [];
set playing(playing) { }, null);
this.sim.playing = playing; });
} }
} }