diff --git a/Readme.md b/Readme.md index fa8abd8..452f329 100644 --- a/Readme.md +++ b/Readme.md @@ -20,3 +20,8 @@ TODO - [ ] 2-touch Pan & Zoom - [ ] Multi-touch Mass Create - [ ] Tool to Clear Traces +- [ ] Undo feature: + - [ ] Undo "Clear Traces" Action + - [ ] Undo "Reset +- [ ] Save to LocalStorage +- [ ] Lossy Rescaling To Widen Zoom (Handling overflow/underflow) diff --git a/config.js b/config.js index 9bb0d4b..117f13c 100644 --- a/config.js +++ b/config.js @@ -9,13 +9,13 @@ export const DISPLAY_ACCELERATION_VECTORS = true; export const DISPLAY_PATH_TRACES = false; // 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_WIDTH = 1.5; export const VELOCITY_VECTOR_ARROWHEAD = true; // 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_WIDTH = 1.5; export const ACCELERATION_VECTOR_ARROWHEAD = true; diff --git a/display.js b/display.js index 6ec320a..b21d3f7 100644 --- a/display.js +++ b/display.js @@ -1,22 +1,7 @@ import { - ACCELERATION_VECTOR_ARROWHEAD, - ACCELERATION_VECTOR_COLOR, - ACCELERATION_VECTOR_SCALE, - ACCELERATION_VECTOR_WIDTH, ARROWHEAD_LENGTH, ARROWHEAD_WIDTH, 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'; export class Display { @@ -86,139 +71,8 @@ export class Display { 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() { - 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}) { diff --git a/object.js b/object.js index d05daab..301c8b7 100644 --- a/object.js +++ b/object.js @@ -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 { mass = 0; density = 1; @@ -25,7 +43,7 @@ export class MassObject { get radius() { // radius should be proportional to cube root of mass - return Math.pow(this.mass / this.density, 1/3); + return Math.pow(this.mass / this.density, 1 / 3); } getAcceleration() { @@ -40,5 +58,146 @@ export class MassObject { 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' + }); + } + + } + } diff --git a/tool/play-pause.js b/tool/play-pause.js index 2c87ecd..540376a 100644 --- a/tool/play-pause.js +++ b/tool/play-pause.js @@ -4,7 +4,11 @@ import {Tool} from '../tool.js'; export class PlayPause extends Tool { playHTML = 'Play'; pauseHTML = 'Pause'; + clearTracesText = 'Clear Traces'; currentTimeEl = undefined; + pauseButton = undefined; + playButton = undefined; + clearTracesEl = undefined; get timeText() { let time = this.sim.time; @@ -36,52 +40,52 @@ 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) { super(toolbar); const currentTime = document.createElement('button'); const pauseButton = 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.div.appendChild(currentTime); this.div.appendChild(pauseButton); this.div.appendChild(playButton); + this.div.appendChild(clearTraces); currentTime.classList.add(TOOL_INFO_CLASSNAME); currentTime.classList.add(WIDE_CLASSNAME); + clearTraces.classList.add(WIDE_CLASSNAME); pauseButton.innerHTML = this.pauseHTML; playButton.innerHTML = this.playHTML; currentTime.innerHTML = this.timeText; + clearTraces.innerHTML = this.clearTracesText; - pauseButton.style.opacity = this.sim.playing ? '100%' : '50%'; - playButton.style.opacity = this.sim.playing ? '50%' : '100%'; + this.updateButtons(); - pauseButton.addEventListener('click', (e) => { - e.stopPropagation(); + pauseButton.addEventListener('click', () => { this.sim.pointer.panning = undefined; - if (this.playing) { - this.playing = false; - pauseButton.style.opacity = '50%'; - playButton.style.opacity = '100%'; + if (this.sim.playing) { + this.sim.playing = false; + this.updateButtons(); } }); playButton.addEventListener('click', () => { - if (!this.playing) { - this.playing = true; - pauseButton.style.opacity = '100%'; - playButton.style.opacity = '50%'; + if (!this.sim.playing) { + this.sim.playing = true; + this.updateButtons(); } }); } - - get playing() { - return this.sim.playing; - } - - set playing(playing) { - this.sim.playing = playing; - } }