diff --git a/config.js b/config.js index 78e39d0..9c7ab36 100644 --- a/config.js +++ b/config.js @@ -1,3 +1,4 @@ +// DISPLAY export const DISPLAY_OBJECTS_INFO = false; export const DISPLAY_CURSOR_INFO = false; export const DISPLAY_CANVAS_SIZE = false; @@ -5,36 +6,54 @@ export const DISPLAY_CURRENT_SCALE = true; export const DISPLAY_CURRENT_MODE = false; export const DISPLAY_VELOCITY_VECTORS = true; export const DISPLAY_ACCELERATION_VECTORS = true; +export const DISPLAY_PATH_TRACES = false; -export const MASS_CREATION_RATE = 1E1; -export const POINTER_HISTORY_SIZE = 15; - +// VELOCITY export const VELOCITY_VECTOR_SCALE = 5E0; export const VELOCITY_VECTOR_COLOR = 'rgb(150, 150, 150)'; // 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_COLOR = 'rgb(0, 255, 0)'; // optionally set to 'object color' -// export const ACCELERATION_VECTOR_COLOR = 'object color'; +export const ACCELERATION_VECTOR_COLOR = 'rgb(0, 128, 0)'; // optionally set to 'object color' export const ACCELERATION_VECTOR_WIDTH = 1.5; export const ACCELERATION_VECTOR_ARROWHEAD = true; -export const MOTION_TIME_SCALE = 1E-4; -export const PAN_VELOCITY_SCALE_FACTOR = 1E-3; +// PATH TRACES +// export const PATH_TRACES_COLOR = 'rgb(128, 128, 0)'; // optionally set to 'object color' +export const PATH_TRACES_COLOR = 'object color'; +export const PATH_TRACES_WIDTH = 1.5; +// SCALING FACTORS +export const MASS_CREATION_RATE = 1E1; +export const POINTER_HISTORY_SIZE = 15; +export const MOTION_TIME_SCALE = 1E-4; +export const PAN_ACCELERATION = 1E1; export const ARROWHEAD_LENGTH = 7; export const ARROWHEAD_WIDTH = 5; export const OFFSCREEN_OBJECT_LINE_SCALE = 7; export const OFFSCREEN_OBJECT_LINE_WIDTH = 2; export const OFFSCREEN_OBJECT_ARROWHEAD_LENGTH = 15; +export const ZOOM_TO_FIT_PADDING = 50; export const ZOOM_IN_FACTOR = 1; export const ZOOM_OUT_FACTOR = -1; export const SCALE_POWER_MAX = 8; export const SCALE_POWER_MIN = -8; export const GRAVITATIONAL_CONSTANT = 1E5; +// CSS CLASS NAMES export const DRAGGABLE_ELEMENT_CLASSNAME = 'lhg-draggable-element'; +// EVENT NAMES +export const EVENT_MODE_LEAVE = 'lhg-mode-leave'; +export const EVENT_MODE_ENTER = 'lhg-mode-enter'; + +// MODES export const MODE_MASS_GENERATION = 'mass-gen'; export const MODE_PAN_VIEW = 'pan-view'; + +// OPTIONS +export const PAUSE_DURING_CREATION = true; +export const PAUSE_DURING_SELECTION = true; +export const MERGE_ON_COLLIDE = true; diff --git a/display.js b/display.js index c43d707..590698a 100644 --- a/display.js +++ b/display.js @@ -13,8 +13,8 @@ import { OFFSCREEN_OBJECT_LINE_WIDTH, OFFSCREEN_OBJECT_ARROWHEAD_LENGTH, DISPLAY_CANVAS_SIZE, - DISPLAY_VELOCITY_VECTORS, - DISPLAY_ACCELERATION_VECTORS, + PATH_TRACES_COLOR, + PATH_TRACES_WIDTH, } from './config.js'; export class Display { @@ -63,6 +63,13 @@ export class Display { }; } + 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; @@ -77,8 +84,7 @@ export class Display { ctx.fillRect(this.viewOrigin.x, this.viewOrigin.y, this.width, this.height); } - drawObject(idx) { - const obj = this.sim.objects.object(idx); + drawObject(obj) { const ctx = this.ctx; const {r, g, b} = obj.color; const {x, y} = obj.position; @@ -90,6 +96,47 @@ export class Display { 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; + const skip = 1; + let skipped = 0; + { + const [{position}] = obj.history; + const x = position.x; + const y = position.y; + + if (Math.abs(x - cx) <= W / 2 && + Math.abs(y - cy) <= H / 2) { + ctx.moveTo(x, y); + dash = true; + } + } + for (let i = 1; i < obj.history.length; i++) { + if (++skipped < skip) continue; + skipped = 0; + 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) { @@ -146,7 +193,7 @@ export class Display { ctx.fill(); // Draw arrow for the velocity - if (DISPLAY_VELOCITY_VECTORS) { + 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); @@ -162,7 +209,7 @@ export class Display { } // Draw arrow for acceleration - if (DISPLAY_ACCELERATION_VECTORS) { + 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); @@ -181,9 +228,7 @@ export class Display { } drawObjects() { - for (let i = 0; i < this.sim.objects.length; i++) { - this.drawObject(i); - } + this.sim.objects.forEachObject(obj => this.drawObject(obj), null); } drawArrow(startX, startY, endX, endY, {style, width, arrowhead, arrowheadLength, fill, ifShort}) { diff --git a/index.html b/index.html index a5c355a..8ce8cfb 100644 --- a/index.html +++ b/index.html @@ -8,7 +8,7 @@ body { background-color: #000; color: #8f8; font-family: monospace; - font-size: 16pt; + font-size: 12pt; overflow: hidden; user-select: none; -webkit-user-select: none; @@ -24,7 +24,9 @@ div[id=simulator] { button { width: 8em; padding-left: 0.5em; - padding-left: 0.5em; + padding-right: 0.5em; + padding-top: 0; + padding-bottom: 0; } button.wide { diff --git a/object.js b/object.js index 3b4f22b..d05daab 100644 --- a/object.js +++ b/object.js @@ -1,5 +1,4 @@ export class MassObject { - index = undefined; mass = 0; density = 1; position = {x: undefined, y: undefined}; @@ -8,10 +7,10 @@ export class MassObject { color = {r: undefined, g: undefined, b: undefined}; created = undefined; forces = []; // [{x, y}] - active = false; + history = []; + alive = true; - constructor(x, y, index) { - this.index = index; + constructor(x, y) { this.position.x = x; this.position.y = y; this.color.r = Math.random() * 256; diff --git a/objects.js b/objects.js index f2efac5..620a96f 100644 --- a/objects.js +++ b/objects.js @@ -4,30 +4,62 @@ import { DISPLAY_OBJECTS_INFO, MOTION_TIME_SCALE, GRAVITATIONAL_CONSTANT, + ZOOM_TO_FIT_PADDING, } from './config.js'; export class Objects { objects = []; creatingObject = undefined; selectedObject = undefined; + paused = false; + panVelocityPaused = undefined; constructor(sim) { this.sim = sim; } + pause() { + if (this.sim.playing) { + this.sim.playing = false; + this.paused = true; + if (this.sim.pointer.panning?.velocity) { + this.panVelocityPaused = {...this.sim.pointer.panning.velocity}; + this.sim.pointer.panning = undefined; + } + } + } + + resume() { + if (this.paused) { + this.sim.playing = true; + this.paused = false; + if (this.panVelocityPaused) { + this.sim.pointer.panning = { + velocity: {...this.panVelocityPaused}, + }; + this.panVelocityPaused = undefined; + } + } + } + // Create an object with mass that grows as pointer is held down createObject(x, y) { const idx = this.objects.length; const obj = new MassObject(x, y, idx); this.creatingObject = idx; this.objects.push(obj); + // Pause the simulation during mass creation; this avoids some complex local dynamics + // TODO: Make this optional (toolbar item to enable) + if (this.sim.getOption('pauseDuring.creation')) { + this.pause(); + } } doneCreatingObject() { if (this.creatingObject !== undefined) { const obj = this.objects[this.creatingObject]; - obj.active = true; this.creatingObject = undefined; + this.resume(); } } @@ -37,10 +69,14 @@ export class Objects { selectObject(i) { this.selectedObject = i; + if (this.sim.getOption('pauseDuring.selection')) { + this.pause(); + } } deselect() { this.selectedObject = undefined; + this.resume(); } getSelectedOrCreating() { @@ -59,8 +95,7 @@ export class Objects { start: {x: undefined, y: undefined}, end: {x: undefined, y: undefined}, }; - for (let i = 0; i < this.objects.length; i++) { - const obj = this.objects[i]; + this.forEachObject(obj => { if (box.start.x === undefined) { box.start = {...obj.position}; box.end = {...obj.position}; @@ -70,19 +105,25 @@ export class Objects { if (obj.position.y < box.start.y) box.start.y = obj.position.y; if (obj.position.y > box.end.y) box.end.y = obj.position.y; } - } + }); + box.start.x -= ZOOM_TO_FIT_PADDING; + box.start.y -= ZOOM_TO_FIT_PADDING; + box.end.x += ZOOM_TO_FIT_PADDING; + box.end.y += ZOOM_TO_FIT_PADDING; return box; } objectAtLocation(x, y) { - for (let i = 0; i < this.objects.length; i++) { - const obj = this.objects[i]; + let idx = undefined; + this.forEachObject((obj, i) => { // If distance to object is less than object's radius, we are touching the object const dist = Math.pow((obj.position.x - x)**2 + (obj.position.y - y)**2, 1/2); if (dist <= obj.radius) { - return i; + idx = i; + return null; } - } + }); + return idx; } handlePointerDown({x, y}) { @@ -106,6 +147,10 @@ export class Objects { // If the cursor moves while creating an object, or while an object is selected, // update the position and velocity of the object const obj = this.getSelectedOrCreating(); + if (this.sim.pointer.panning?.velocity) { + vx += this.sim.pointer.panning.velocity.x; + vy += this.sim.pointer.panning.velocity.y; + } if (obj !== undefined) { obj.position.x = x; obj.position.y = y; @@ -114,16 +159,25 @@ export class Objects { } } + // cb: (obj, idx) => {} + // TODO: Reducer + forEachObject(cb, alive = true, startWith = 0) { + for (let i = startWith; i < this.objects.length; i++) { + const obj = this.objects[i]; + if (alive === null || alive == obj.alive) { + const ret = cb(obj, i); + if (ret === null) break; + } + } + } + computeForces() { if (this.objects.length < 2) return; - for (let i = 0; i < this.objects.length; i++) { - this.objects[i].forces = []; - } - for (let i = 0; i < this.objects.length; i++) { - const A = this.objects[i]; - for (let j = i + 1; j < this.objects.length; j++) { - const B = this.objects[j]; - if (!A.active || !B.active) continue; + this.forEachObject(obj => { + obj.forces = []; + }); + this.forEachObject((A, i) => { + this.forEachObject(B => { const dx = (B.position.x - A.position.x); const dy = (B.position.y - A.position.y); const dSquared = dx ** 2 + dy ** 2; @@ -133,12 +187,15 @@ export class Objects { const Fy = F * dy / d; A.forces.push({ x: Fx, y: Fy }); B.forces.push({ x: -Fx, y: -Fy }); - } - } + }, true, i + 1); + }); + // Also compute acceleration + this.forEachObject(obj => { + obj.acceleration = obj.getAcceleration(); + }); } computeFrame(elapsedTime) { - elapsedTime = elapsedTime * MOTION_TIME_SCALE; // If we're creating an object, increment its mass // with the mass creation rate accelerating over time if (this.creatingObject !== undefined) { @@ -151,51 +208,86 @@ export class Objects { this.computeForces(); if (this.sim.playing) { - // Generate predicted positions (Velocity verlet method) - for (let i = 0; i < this.objects.length; i++) { - const obj = this.objects[i]; - obj.currentAcceleration = obj.getAcceleration(); + // Predict positions (Velocity verlet method) + this.forEachObject(obj => { + obj.currentAcceleration = {...obj.acceleration}; + obj.position.x += elapsedTime * (obj.velocity.x + 1/2 * obj.currentAcceleration.x * elapsedTime); obj.position.y += elapsedTime * (obj.velocity.y + 1/2 * obj.currentAcceleration.y * elapsedTime); + }); + + // Collisions + if (this.sim.getOption('collision.merge')) { + this.forEachObject((A, i) => { + this.forEachObject((B, j) => { + const dx = (B.position.x - A.position.x); + const dy = (B.position.y - A.position.y); + const dSquared = dx ** 2 + dy ** 2; + const d = Math.sqrt(dSquared); + if (d < A.radius + B.radius) { + // Merge B into A: + // Set position = center of mass + // Set velocity = total momentum / total mass + // Combine forces + // Add masses + // Average color + // If B was selected or being created, select A instead + // Remove B using splice + A.position.x = (A.position.x * A.mass + B.position.x * B.mass) / (A.mass + B.mass); + A.position.y = (A.position.y * A.mass + B.position.y * B.mass) / (A.mass + B.mass); + A.velocity.x = (A.velocity.x * A.mass + B.velocity.x * B.mass) / (A.mass + B.mass); + A.velocity.y = (A.velocity.y * A.mass + B.velocity.y * B.mass) / (A.mass + B.mass); + A.forces.push(...B.forces); + A.mass += B.mass; + A.color = { + r: (A.mass * A.color.r + B.mass * B.color.r) / (A.mass + B.mass), + g: (A.mass * A.color.g + B.mass * B.color.g) / (A.mass + B.mass), + b: (A.mass * A.color.b + B.mass * B.color.b) / (A.mass + B.mass), + }; + if (this.creatingObject === j) this.creatingObject = i; + if (this.selectedObject === j) this.selectedObject = i; + B.alive = false; + B.forces = []; + } + }, true, i + 1); + }); } // Recompute forces this.computeForces(); - // Generated predicted velocities - for (let i = 0; i < this.objects.length; i++) { - const obj = this.objects[i]; - const acceleration = obj.getAcceleration(); + // Predict velocities + this.forEachObject(obj => { + const acceleration = {...obj.acceleration}; obj.acceleration = { x: (obj.currentAcceleration.x + acceleration.x) / 2, y: (obj.currentAcceleration.y + acceleration.y) / 2, }; obj.velocity.x += obj.acceleration.x * elapsedTime; obj.velocity.y += obj.acceleration.y * elapsedTime; - } - } else { - for (let i = 0; i < this.objects.length; i++) { - const obj = this.objects[i]; - obj.acceleration = obj.getAcceleration(); - } + + // Append to object history + // TODO: enforce object history length + // TODO: store object color changes in history + obj.history.push({position: {...obj.position}}); + }); } // Display objects info if (DISPLAY_OBJECTS_INFO) { - for (let i = 0; i < this.objects.length; i++) { - const obj = this.objects[i]; + this.forEachObject((obj, i) => { const speed = Math.pow(obj.velocity.x ** 2 + obj.velocity.y ** 2, 1/2); // Invert y so that the angle is counterclockwise from x-axis const direction = Math.atan2(-obj.velocity.y, obj.velocity.x) * 180 / Math.PI; this.sim.info[`Object ${i}`] = [ - `${obj.position.x}, `, - `${obj.position.y}, `, + `${obj.position.x.toPrecision(6)}, `, + `${obj.position.y.toPrecision(6)}, `, `${obj.mass.toPrecision(6)} kg, `, `${speed.toPrecision(2)} m/s, ${direction.toPrecision(2)}°`, ]; - } + }); } } } diff --git a/overlay.js b/overlay.js index cbb46b9..52ca549 100644 --- a/overlay.js +++ b/overlay.js @@ -10,6 +10,7 @@ export class Overlay { this.sim.div.appendChild(infoBox); this.infoBox = infoBox; infoBox.style.position = 'relative'; + infoBox.style.display = 'inline-block'; infoBox.style.top = 0; infoBox.style.left = 0; infoBox.width = 'fit-content'; diff --git a/pointer.js b/pointer.js index 5eb9341..8a2c78f 100644 --- a/pointer.js +++ b/pointer.js @@ -6,7 +6,6 @@ import { DRAGGABLE_ELEMENT_CLASSNAME, MODE_MASS_GENERATION, MODE_PAN_VIEW, - PAN_VELOCITY_SCALE_FACTOR, } from './config.js'; function dispatchEvent(target, eventType, data) { @@ -85,10 +84,10 @@ export class Pointer { // Monitor wheel events el.addEventListener('wheel', e => { - const factor = e.deltaY > 0 ? ZOOM_IN_FACTOR : ZOOM_OUT_FACTOR; - this.sim.scheduleZoom({x: e.clientX, y: e.clientY}, factor); + const factor = e.deltaY < 0 ? ZOOM_IN_FACTOR : ZOOM_OUT_FACTOR; + const {x, y} = this.sim.screenToSim(e.clientX, e.clientY); + this.sim.scheduleZoom({x, y}, factor); }); - } getPointerVelocity() { @@ -99,9 +98,25 @@ export class Pointer { const start = this.pointerHistory[0]; const end = this.pointerHistory[this.pointerHistory.length - 1]; const dt = (end.t - start.t) / 1000; + // Bonus scale factor for pointer power return { - x: (end.x - start.x) / dt, - y: (end.y - start.y) / dt, + x: (end.x - start.x) / dt * this.sim.display.scale, + y: (end.y - start.y) / dt * this.sim.display.scale, + dt + }; + } + + getPointerAcceleration() { + // Average over pointer history + if (this.pointerHistory.length < 2) { + return {x: 0, y: 0, dt: 1}; + } + const start = this.pointerHistory[0]; + const end = this.pointerHistory[this.pointerHistory.length - 1]; + const dt = (end.t - start.t) / 1000; + return { + x: (end.v.x - start.v.x) / dt, + y: (end.v.y - start.v.y) / dt, dt }; } @@ -112,25 +127,25 @@ export class Pointer { updatePointer({x, y}) { const t = document.timeline.currentTime; - this.pointerHistory.push({x, y, t}); - if (this.pointerHistory.length > POINTER_HISTORY_SIZE) { + while (this.pointerHistory.length >= POINTER_HISTORY_SIZE) { this.pointerHistory.shift(); } + const v = this.getPointerVelocity(); + const a = this.getPointerAcceleration(); + this.pointerHistory.push({t, x, y, v, a}); } handlePointerDown({x: clientX, y: clientY}) { + this.updatePointer({x: clientX, y: clientY}); if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) { + // this.panning = undefined; const {x, y} = this.sim.screenToSim(clientX, clientY) this.sim.objects.handlePointerDown({x, y}); } else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) { - this.panning = { - gathering: true, - viewOriginStart: this.sim.display.viewOrigin, - pointerStart: {x: clientX, y: clientY}, - pointerCurrent: {x: clientX, y: clientY}, - velocity: this.getPointerVelocity(), - }; + this.panning = this.panning || {}; + this.panning.gathering = true; + this.panning.velocity = {x: 0, y: 0}; } } @@ -163,8 +178,12 @@ export class Pointer { } else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) { if (this.panning?.gathering) { this.updatePointer({ x: clientX, y: clientY }); - this.panning.pointerCurrent = {x: clientX, y: clientY}; - this.panning.velocity = this.getPointerVelocity(); + const velocity = this.getPointerVelocity(); + const acceleration = this.getPointerAcceleration(); + // Convet to sim coordinates + // Let's try incorporating pointer acceleration + this.panning.velocity.x = velocity.x + acceleration.x * velocity.dt; + this.panning.velocity.y = velocity.y + acceleration.y * velocity.dt; } } } @@ -177,20 +196,11 @@ export class Pointer { } // Apply update to viewOrigin based on panning - if (!this.sim.isCurrentMode(MODE_PAN_VIEW)) { - this.panning = undefined; - return; - } if (this.panning) { - const {pointerStart, pointerCurrent, viewOriginStart, velocity} = this.sim.pointer.panning; + const {velocity} = this.panning; // Convert pointer velocity to sim internal scale - const vx = velocity.x / this.sim.display.scale; - const vy = velocity.y / this.sim.display.scale; - this.panning.viewOriginStart = this.sim.display.viewOrigin; - this.panning.pointerStart = pointerCurrent; - // const speed = Math.sqrt(velocity.x ** 2 + velocity.y ** 2); - this.sim.display.viewOrigin.x -= vx * elapsedTime * PAN_VELOCITY_SCALE_FACTOR; - this.sim.display.viewOrigin.y -= vy * elapsedTime * PAN_VELOCITY_SCALE_FACTOR; + this.sim.display.viewOrigin.x -= velocity.x * elapsedTime; + this.sim.display.viewOrigin.y -= velocity.y * elapsedTime; } } } diff --git a/simulator.js b/simulator.js index c500e81..fb50ad0 100644 --- a/simulator.js +++ b/simulator.js @@ -6,11 +6,14 @@ import { Toolbar } from './toolbar.js'; import { PlayPause } from './tool/play-pause.js'; import { Zoom } from './tool/zoom.js'; import { ModeSwitch } from './tool/modes.js'; +import { Options } from './tool/options.js'; import { SCALE_POWER_MAX, SCALE_POWER_MIN, DISPLAY_CURRENT_SCALE, DISPLAY_CURRENT_MODE, + MOTION_TIME_SCALE, + MODE_PAN_VIEW, } from './config.js'; export class Sim { @@ -25,6 +28,7 @@ export class Sim { objects = undefined; isCurrentMode = () => false; + getOption = () => undefined; init(divId) { this.divId = divId; @@ -34,13 +38,15 @@ export class Sim { this.display = new Display(this); this.overlay = new Overlay(this); this.objects = new Objects(this); - this.pointer = new Pointer(this); this.toolbar = new Toolbar(this); // Set up toolbar this.toolbar.addTool(new Zoom(this.toolbar)); this.toolbar.addTool(new PlayPause(this.toolbar)); this.toolbar.addTool(new ModeSwitch(this.toolbar)); + this.toolbar.addTool(new Options(this.toolbar)); + + this.pointer = new Pointer(this); // Initiate main loop this.time = document.timeline.currentTime; @@ -49,16 +55,19 @@ export class Sim { // It's better not to change the scale in the middle of possible frame calculations, // so use this to schedule it and let the event loop pick it up. - scheduleZoom({x, y}, factor) { - this.nextZoom = {x, y, factor}; + // velocity should be in Sim coordinate scale + scheduleZoom({x, y}, factor, velocity) { + this.nextZoom = {x, y, factor, velocity}; } - zoom({x: screenX, y: screenY, factor}) { - const {x, y} = this.screenToSim(screenX, screenY); + // x, y should be in Sim coordinates + // velocity should be in Sim coordinate scale + zoom({x, y, factor, velocity}) { // x, y are the mouse coordinates, which should be the center of the new view frame // the new view origin should be x, y minus half the new view width and height // compute new scale this.display.scalePower += factor; + // TODO: Lossy rescaling to expand zoom range if (this.display.scalePower > SCALE_POWER_MAX) this.display.scalePower = SCALE_POWER_MAX; if (this.display.scalePower < SCALE_POWER_MIN) this.display.scalePower = SCALE_POWER_MIN; // compute coordinates of new view frame @@ -66,9 +75,17 @@ export class Sim { this.display.viewOrigin.y = y - this.display.height / 2; this.pointer.clearPointerHistory(); - if (this.pointer.panning) { - this.pointer.panning = undefined; - // TODO: Maybe rescale velocity + + if (this.playing && velocity) { + // Switch to pan mode + this.setCurrentMode(MODE_PAN_VIEW); + + this.pointer.panning = { + velocity: { + x: -velocity.x, + y: -velocity.y, + } + }; } } @@ -87,7 +104,7 @@ export class Sim { // Main loop loop(currentTime) { - const elapsedTime = currentTime - this.time; + const elapsedTime = (currentTime - this.time) * MOTION_TIME_SCALE; this.time = currentTime; if (DISPLAY_CURRENT_MODE) { diff --git a/tool/modes.js b/tool/modes.js index d4ec7de..4e9a6b7 100644 --- a/tool/modes.js +++ b/tool/modes.js @@ -3,6 +3,8 @@ import { Tool } from '../tool.js'; import { MODE_MASS_GENERATION, MODE_PAN_VIEW, + EVENT_MODE_LEAVE, + EVENT_MODE_ENTER, } from '../config.js'; export class ModeSwitch extends Tool { @@ -16,9 +18,6 @@ export class ModeSwitch extends Tool { constructor(toolbar) { super(toolbar); - const [[currentModeID, _]] = this.modes; - this.currentMode = currentModeID; - const modesDiv = document.createElement('div'); const titleDiv = document.createElement('div'); @@ -41,18 +40,19 @@ export class ModeSwitch extends Tool { button.innerHTML = `