work in progress

This commit is contained in:
Ladd 2025-12-31 15:56:01 -06:00
parent fe6a263164
commit f5512c1be3
11 changed files with 162 additions and 117 deletions

View File

@ -1,11 +1,9 @@
// VELOCITY // VELOCITY
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 = 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;
@ -54,3 +52,4 @@ export const MODE_OBJECT_SELECT = 'select';
// LOCAL STORAGE PREFIXES/SUFFIXES // LOCAL STORAGE PREFIXES/SUFFIXES
export const TOOLBAR_EXPANDED_SUFFIX = 'lhg-toolbar-expanded'; export const TOOLBAR_EXPANDED_SUFFIX = 'lhg-toolbar-expanded';

View File

@ -67,10 +67,6 @@ 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);
} }
drawObjects() {
this.sim.objects.forEachObject(obj => obj.drawObject(this.sim), {alive: null});
}
drawArrow(startX, startY, endX, endY, {style, width, arrowhead, arrowheadLength, fill, ifShort}) { drawArrow(startX, startY, endX, endY, {style, width, arrowhead, arrowheadLength, fill, ifShort}) {
const ctx = this.ctx; const ctx = this.ctx;
ctx.strokeStyle = style; ctx.strokeStyle = style;
@ -139,17 +135,12 @@ export class Display {
ctx.resetTransform(); ctx.resetTransform();
} }
computePanning(elapsedTime) { frame(elapsedTime) {
// Add another entry for the current pointer position
const { const {
pointerHistory, touchStart: start,
panTouchStart: start, touchLatest: latest,
panTouchLatest: latest,
} = this.sim.pointer ?? {}; } = this.sim.pointer ?? {};
if (pointerHistory?.length) {
const currentPointer = pointerHistory[pointerHistory.length - 1];
this.sim.pointer.updatePointer(currentPointer);
}
if (start && latest) { if (start && latest) {
// Direct translate // Direct translate
this.viewOrigin.x = start.viewOrigin.x - (latest.x - start.x) / this.scale; this.viewOrigin.x = start.viewOrigin.x - (latest.x - start.x) / this.scale;
@ -158,9 +149,11 @@ export class Display {
// Apply update to viewOrigin based on panning // Apply update to viewOrigin based on panning
const { velocity } = this.sim.panning; const { velocity } = this.sim.panning;
// TODO: something with time scale? Panning is too fast. // TODO: something with time scale? Panning is too fast.
this.viewOrigin.x -= velocity.x * elapsedTime / 1000; // millisecond conversion? this.viewOrigin.x += velocity.x * elapsedTime;
this.viewOrigin.y -= velocity.y * elapsedTime / 1000; this.viewOrigin.y += velocity.y * elapsedTime;
} }
}
// Clear canvas in preparation for other modules to render this frame
this.fillCanvas();
}
} }

View File

@ -1,7 +1,6 @@
import { import {
ACCELERATION_VECTOR_ARROWHEAD, ACCELERATION_VECTOR_ARROWHEAD,
ACCELERATION_VECTOR_COLOR, ACCELERATION_VECTOR_COLOR,
ACCELERATION_VECTOR_SCALE,
ACCELERATION_VECTOR_WIDTH, ACCELERATION_VECTOR_WIDTH,
OFFSCREEN_OBJECT_ARROWHEAD_LENGTH, OFFSCREEN_OBJECT_ARROWHEAD_LENGTH,
OFFSCREEN_OBJECT_LINE_SCALE, OFFSCREEN_OBJECT_LINE_SCALE,
@ -12,11 +11,12 @@ import {
PATH_TRACES_WIDTH, PATH_TRACES_WIDTH,
VELOCITY_VECTOR_ARROWHEAD, VELOCITY_VECTOR_ARROWHEAD,
VELOCITY_VECTOR_COLOR, VELOCITY_VECTOR_COLOR,
VELOCITY_VECTOR_SCALE,
VELOCITY_VECTOR_WIDTH, VELOCITY_VECTOR_WIDTH,
} from './config.js'; } from './config.js';
export class MassObject { export class MassObject {
sim = undefined;
id = undefined;
mass = 0; mass = 0;
density = 1; density = 1;
position = {x: undefined, y: undefined}; position = {x: undefined, y: undefined};
@ -28,7 +28,9 @@ export class MassObject {
history = []; history = [];
alive = true; alive = true;
constructor(x, y) { constructor(sim, x, y) {
this.sim = sim;
this.id = crypto.randomUUID();
this.position.x = x; this.position.x = x;
this.position.y = y; this.position.y = y;
this.color.r = Math.random() * 256; this.color.r = Math.random() * 256;
@ -86,7 +88,7 @@ export class MassObject {
const opacity = dashedTraces ? PATH_TRACES_DASHED_OPACITY : PATH_TRACES_OPACITY; const opacity = dashedTraces ? PATH_TRACES_DASHED_OPACITY : PATH_TRACES_OPACITY;
ctx.strokeStyle = PATH_TRACES_COLOR === 'object color' ? ctx.strokeStyle = PATH_TRACES_COLOR === 'object color' ?
`rgba(${r}, ${g}, ${b}, ${opacity})` : PATH_TRACES_COLOR; `rgba(${r}, ${g}, ${b}, ${opacity})` : PATH_TRACES_COLOR;
ctx.lineWidth = PATH_TRACES_WIDTH / this.scale; ctx.lineWidth = PATH_TRACES_WIDTH / this.sim.display.scale;
ctx.beginPath(); ctx.beginPath();
let dash = false; let dash = false;
for (let i = 0; i < this.history.length; i++) { for (let i = 0; i < this.history.length; i++) {
@ -142,8 +144,8 @@ export class MassObject {
const arrowDirection = Math.atan2(py - cy, px - cx); const arrowDirection = Math.atan2(py - cy, px - cx);
// Length of arrow based on distance (logarithmic scale) // Length of arrow based on distance (logarithmic scale)
const distance = Math.sqrt((x - px) ** 2, (y - py) ** 2) * this.scale; const distance = Math.sqrt((x - px) ** 2, (y - py) ** 2);
const arrowLength = Math.log(distance) * OFFSCREEN_OBJECT_LINE_SCALE / this.scale; const arrowLength = Math.log(distance) * OFFSCREEN_OBJECT_LINE_SCALE / this.sim.display.scale;
const startAx = px - arrowLength * Math.cos(arrowDirection); const startAx = px - arrowLength * Math.cos(arrowDirection);
const startAy = py - arrowLength * Math.sin(arrowDirection); const startAy = py - arrowLength * Math.sin(arrowDirection);
sim.display.drawArrow(startAx, startAy, px, py, { sim.display.drawArrow(startAx, startAy, px, py, {
@ -165,9 +167,19 @@ export class MassObject {
// Draw arrow for the velocity // Draw arrow for the velocity
if (sim.getOption('display.velocity')) { if (sim.getOption('display.velocity')) {
const speed = Math.sqrt(vx ** 2 + vy ** 2); // If this object is being dragged by the user,
const endVx = x + VELOCITY_VECTOR_SCALE * vx / speed * Math.log(speed); // show the pointer velocity instead of object velocity
const endVy = y + VELOCITY_VECTOR_SCALE * vy / speed * Math.log(speed); const vecScale = this.sim.getOption('param.velocityScale');
const selected = this.sim.system.getSelectedOrCreating();
const velocity = selected?.id === this.id ?
this.sim.pointer.latestVelocity ?? {x: 0, y: 0} :
{x: vx, y: vy};
const speed = Math.sqrt(velocity.x ** 2, velocity.y ** 2);
const arrowDirection = Math.atan2(velocity.y, velocity.x);
// Prevent negative numbers by adding e
const arrowLength = Math.log(speed + 3) * vecScale / this.sim.display.scale;
const endVx = x + arrowLength * Math.cos(arrowDirection);
const endVy = y + arrowLength * Math.sin(arrowDirection);
const style = VELOCITY_VECTOR_COLOR === 'object color' ? const style = VELOCITY_VECTOR_COLOR === 'object color' ?
`rgb(${r}, ${g}, ${b})` : VELOCITY_VECTOR_COLOR; `rgb(${r}, ${g}, ${b})` : VELOCITY_VECTOR_COLOR;
sim.display.drawArrow(x, y, endVx, endVy, { sim.display.drawArrow(x, y, endVx, endVy, {
@ -181,11 +193,13 @@ export class MassObject {
// Draw arrow for acceleration // Draw arrow for acceleration
if (sim.getOption('display.acceleration')) { if (sim.getOption('display.acceleration')) {
const vecScale = this.sim.getOption('param.accelerationScale');
const accelerationMagnitude = Math.sqrt(acceleration.x ** 2 + acceleration.y ** 2); const accelerationMagnitude = Math.sqrt(acceleration.x ** 2 + acceleration.y ** 2);
const endAx = x + ACCELERATION_VECTOR_SCALE * acceleration.x / const arrowDirection = Math.atan2(acceleration.y, acceleration.x);
accelerationMagnitude * Math.log(accelerationMagnitude); // Prevent negative numbers by adding e
const endAy = y + ACCELERATION_VECTOR_SCALE * acceleration.y / const arrowLength = Math.log(accelerationMagnitude + 3) * vecScale / this.sim.display.scale;
accelerationMagnitude * Math.log(accelerationMagnitude); const endAx = x + arrowLength * Math.cos(arrowDirection);
const endAy = y + arrowLength * Math.sin(arrowDirection);
const style = ACCELERATION_VECTOR_COLOR === 'object color' ? const style = ACCELERATION_VECTOR_COLOR === 'object color' ?
`rgb(${r}, ${g}, ${b})` : ACCELERATION_VECTOR_COLOR; `rgb(${r}, ${g}, ${b})` : ACCELERATION_VECTOR_COLOR;
sim.display.drawArrow(x, y, endAx, endAy, { sim.display.drawArrow(x, y, endAx, endAy, {

View File

@ -6,6 +6,7 @@ export class Options {
sim = undefined; sim = undefined;
options = undefined; options = undefined;
values = {}; values = {};
undefinedObj = {_undefined: true};
getStorageKey(path) { getStorageKey(path) {
return `${path}:options`; return `${path}:options`;
@ -28,16 +29,24 @@ export class Options {
if (value === undefined) { if (value === undefined) {
value = defaultValue; value = defaultValue;
} }
console.log('initializing option', {path, defaultValue, value});
this.values[path] = value; this.values[path] = value;
} }
} }
} }
toStored(value) { toStored(value) {
if (value === undefined) {
return JSON.stringify(this.undefinedObj);
}
return JSON.stringify(value); return JSON.stringify(value);
} }
// value: string
fromStored(value) { fromStored(value) {
if (value === JSON.stringify(this.undefinedObj)) {
return undefined;
}
return JSON.parse(value); return JSON.parse(value);
} }

View File

@ -12,7 +12,7 @@ export class Overlay {
infoBox.classList.add(OVERLAY_INFO_BOX_CLASSNAME); infoBox.classList.add(OVERLAY_INFO_BOX_CLASSNAME);
} }
renderInfo() { frame() {
this.infoBox.innerHTML = ''; this.infoBox.innerHTML = '';
const table = document.createElement('table'); const table = document.createElement('table');
for (let [k, v] of Object.entries(this.sim.info)) { for (let [k, v] of Object.entries(this.sim.info)) {

View File

@ -13,8 +13,8 @@ export class Pointer {
sim = undefined; sim = undefined;
pointerHistory = []; pointerHistory = [];
panTouchStart = undefined; // {x: undefined, y: undefined, t: undefined}; touchStart = undefined; // {x: undefined, y: undefined, t: undefined};
panTouchLatest = undefined; // {x: undefined, y: undefined, t: undefined}; touchLatest = undefined; // {x: undefined, y: undefined, t: undefined};
suppressClick = false; suppressClick = false;
constructor(sim) { constructor(sim) {
@ -61,7 +61,7 @@ export class Pointer {
getPointerVelocity(points = POINTER_HISTORY_SIZE) { getPointerVelocity(points = POINTER_HISTORY_SIZE) {
// Average over pointer history // Average over pointer history
if (this.pointerHistory.length < 2) { if (this.pointerHistory.length < 2) {
return this.latestPointerVelocity ?? {x: 0, y: 0, dt: 1}; return this.latestVelocity ?? {x: 0, y: 0, dt: 1};
} }
points = Math.min(points, POINTER_HISTORY_SIZE, this.pointerHistory.length); points = Math.min(points, POINTER_HISTORY_SIZE, this.pointerHistory.length);
const start = this.pointerHistory[this.pointerHistory.length - points]; const start = this.pointerHistory[this.pointerHistory.length - points];
@ -79,7 +79,7 @@ export class Pointer {
} }
updatePointer({x, y}) { updatePointer({x, y}) {
const t = document.timeline.currentTime; const t = this.sim.rawTime;
while (this.pointerHistory.length >= POINTER_HISTORY_SIZE) { while (this.pointerHistory.length >= POINTER_HISTORY_SIZE) {
this.pointerHistory.shift(); this.pointerHistory.shift();
} }
@ -87,7 +87,7 @@ export class Pointer {
this.pointerHistory.push({t, x, y, v}); this.pointerHistory.push({t, x, y, v});
} }
get latestPointerVelocity() { get latestVelocity() {
const latestPointer = this.pointerHistory[this.pointerHistory.length - 1]; const latestPointer = this.pointerHistory[this.pointerHistory.length - 1];
return latestPointer?.v; return latestPointer?.v;
} }
@ -98,16 +98,21 @@ export class Pointer {
if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) { if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) {
const {x, y} = this.sim.screenToSim(clientX, clientY) const {x, y} = this.sim.screenToSim(clientX, clientY)
this.sim.objects.handlePointerDown({x, y}); this.sim.system.handlePointerDown({x, y});
} else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) { } else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) {
this.panTouchStart = { this.touchStart = {
x: clientX, x: clientX,
y: clientY, y: clientY,
t: document.timeline.currentTime, t: this.sim.rawTime,
viewOrigin: {...this.sim.display.viewOrigin}, viewOrigin: {...this.sim.display.viewOrigin},
}; };
this.panTouchLatest = {...this.panTouchStart}; this.touchLatest = {
...this.touchStart,
dx: 0,
dy: 0,
dt: 0,
};
} else if (this.sim.isCurrentMode(MODE_OBJECT_SELECT)) { } else if (this.sim.isCurrentMode(MODE_OBJECT_SELECT)) {
// TODO: Start a selection box // TODO: Start a selection box
@ -117,16 +122,15 @@ export class Pointer {
handlePointerUp({x: clientX, y: clientY}) { handlePointerUp({x: clientX, y: clientY}) {
if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) { if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) {
const {x, y} = this.sim.screenToSim(clientX, clientY); const {x, y} = this.sim.screenToSim(clientX, clientY);
this.sim.objects.handlePointerUp({x, y}); this.sim.system.handlePointerUp({x, y});
} else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) { } else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) {
// Set panning velocity // Set panning velocity
if (this.panTouchStart && this.panTouchLatest) { if (this.touchStart && this.touchLatest) {
const dt = (this.panTouchLatest.t - this.panTouchStart.t) / 1000; if (!this.touchLatest.dt) {
if (!dt) {
this.sim.panning = undefined; this.sim.panning = undefined;
} else { } else {
const v = {...this.latestPointerVelocity}; const v = {...this.latestVelocity};
// Convert pointer velocity to simulation scale // Convert pointer velocity to simulation scale
v.x /= this.sim.display.scale; v.x /= this.sim.display.scale;
v.y /= this.sim.display.scale; v.y /= this.sim.display.scale;
@ -136,7 +140,7 @@ export class Pointer {
}; };
} }
this.panTouchStart = undefined; this.touchStart = undefined;
} }
} }
} }
@ -147,23 +151,32 @@ export class Pointer {
this.updatePointer({x: clientX, y: clientY}); this.updatePointer({x: clientX, y: clientY});
if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) { if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) {
// Convert pointer velocity to simulation scale
const vx = this.latestPointerVelocity.x / this.sim.display.scale;
const vy = this.latestPointerVelocity.y / this.sim.display.scale;
const {x, y} = this.sim.screenToSim(clientX, clientY); const {x, y} = this.sim.screenToSim(clientX, clientY);
this.sim.objects.handlePointerMove({x, y, vx, vy}); this.sim.system.handlePointerMove({x, y});
} else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) { } else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) {
if (this.panTouchStart) { if (this.touchStart) {
// Event loop should be able to read // Event loop should be able to read
this.panTouchLatest = { this.touchLatest = {
x: clientX, x: clientX,
y: clientY, y: clientY,
t: this.sim.rawTime, t: this.sim.rawTime,
dx: clientX - this.touchStart.x,
dy: clientY - this.touchStart.y,
dt: this.sim.rawTime - this.touchStart.t,
}; };
} }
} }
} }
frame() {
// Add another entry for the current pointer position
const { pointerHistory } = this.sim.pointer ?? {};
console.log('pointer history length', pointerHistory?.length);
if (pointerHistory?.length) {
const currentPointer = pointerHistory[pointerHistory.length - 1];
this.sim.pointer.updatePointer(currentPointer);
console.log('updating in case of idle pointer', currentPointer.x, currentPointer.y);
}
}
} }

View File

@ -4,10 +4,12 @@ export const simOptions = {
selection: ['Pause While Selecting', 'boolean', true], selection: ['Pause While Selecting', 'boolean', true],
}, },
display: { display: {
velocity: ['Velocity Vector', 'boolean', true],
acceleration: ['Accel Vector', 'boolean', true],
traces: ['Path Trace', 'boolean', true], traces: ['Path Trace', 'boolean', true],
dashedTraces: ['Dashed', 'boolean', false, {tall: true}], dashedTraces: ['Dashed', 'boolean', false, {tall: true}],
velocity: ['Velocity Vector', 'boolean', true],
acceleration: ['Accel Vector', 'boolean', true],
velocityScale: ['Velocity<br>Vec Scale', 'number', 20],
accelerationScale: ['Accel<br>Vec Scale', 'number', 20],
}, },
collision: { collision: {
merge: ['Merge Masses<br>on Collision', 'boolean', true, {wide: true}], merge: ['Merge Masses<br>on Collision', 'boolean', true, {wide: true}],
@ -20,8 +22,9 @@ export const simOptions = {
}, },
debug: { debug: {
objectsInfo: ['Objects Info', 'boolean', false], objectsInfo: ['Objects Info', 'boolean', false],
aliveObjects: ['Alive Only', 'boolean', false],
cursorInfo: ['Cursor Info', 'boolean', false], cursorInfo: ['Cursor Info', 'boolean', false],
frameRate: ['Frame Rate', 'boolean', false, {wide: true}], frameRate: ['Frame Rate', 'boolean', false],
currentMode: ['Current Mode', 'boolean', false], currentMode: ['Current Mode', 'boolean', false],
panningInfo: ['Panning Info', 'boolean', false], panningInfo: ['Panning Info', 'boolean', false],
}, },

View File

@ -5,7 +5,7 @@ import {
SCALE_POWER_MIN, SCALE_POWER_MIN,
} from './config.js'; } from './config.js';
import { Display } from './display.js'; import { Display } from './display.js';
import { Objects } from './objects.js'; import { System } from './system.js';
import { Options } from './options.js'; import { Options } from './options.js';
import { simOptions } from './sim-options.js'; import { simOptions } from './sim-options.js';
import { initializeTools } from './sim-tools.js'; import { initializeTools } from './sim-tools.js';
@ -14,12 +14,13 @@ export class Sim {
info = {}; info = {};
rawTime = undefined; rawTime = undefined;
time = undefined; time = undefined;
timeScale = undefined;
nextZoom = undefined; nextZoom = undefined;
playing = true; playing = true;
recentFrames = []; recentFrames = [];
frameRate = 0; frameRate = 0;
objects = undefined; system = undefined;
display = undefined; display = undefined;
overlay = undefined; overlay = undefined;
pointer = undefined; pointer = undefined;
@ -31,6 +32,7 @@ export class Sim {
getCurrentMode = () => undefined; getCurrentMode = () => undefined;
setCurrentMode = () => undefined; setCurrentMode = () => undefined;
getOption = () => undefined; getOption = () => undefined;
setOption = () => undefined;
onModeEnter = () => undefined; onModeEnter = () => undefined;
onModeLeave = () => undefined; onModeLeave = () => undefined;
@ -41,8 +43,8 @@ export class Sim {
const oldest = rfs[0]; const oldest = rfs[0];
const newest = rfs[rfs.length - 1]; const newest = rfs[rfs.length - 1];
const count = rfs.length; const count = rfs.length;
const duration = (newest - oldest) / 1000; // ms to s const duration = (newest - oldest);
this.frameRate = count / duration; this.frameRate = 1000 * count / duration; // Converting from ms to s
if (duration >= FRAMERATE_SAMPLE_DURATION) { if (duration >= FRAMERATE_SAMPLE_DURATION) {
rfs.shift(); rfs.shift();
} }
@ -55,7 +57,7 @@ export class Sim {
this.options = new Options(this, simOptions); this.options = new Options(this, simOptions);
this.display = new Display(this); this.display = new Display(this);
this.objects = new Objects(this); this.system = new System(this);
initializeTools(this); initializeTools(this);
@ -90,10 +92,7 @@ export class Sim {
if (this.playing && velocity) { if (this.playing && velocity) {
this.panning = { this.panning = {
velocity: { velocity: { ...velocity }
x: -velocity.x,
y: -velocity.y,
}
}; };
} }
@ -129,11 +128,9 @@ export class Sim {
// Main loop // Main loop
loop(currentTime) { loop(currentTime) {
this.markFrame(currentTime); this.markFrame(currentTime);
const timeScale = this.getOption('param.timeScale'); this.timeScale = this.getOption('param.timeScale');
// elapsedTime in milliseconds const elapsedTime = (currentTime - this.rawTime) * this.timeScale;
// rawTime in milliseconds
const elapsedTime = (currentTime - this.rawTime) / timeScale;
this.rawTime = currentTime; this.rawTime = currentTime;
if (this.playing) { if (this.playing) {
@ -156,19 +153,18 @@ export class Sim {
if (this.getOption('debug.panningInfo')) { if (this.getOption('debug.panningInfo')) {
const {x, y} = this.panning?.velocity ?? {}; const {x, y} = this.panning?.velocity ?? {};
this.info['Panning Velocity'] = [`${x?.toPrecision(6)}, `, y?.toPrecision(6)]; this.info['Panning Velocity'] = [`${x?.toPrecision(6)}, `, y?.toPrecision(6)];
const { centerOfMass } = this.objects.computeSystemCenter(); const { centerOfMass } = this.system.computeSystemCenter();
this.info['Center of Mass'] = [`${centerOfMass.x.toPrecision(6)}, `, centerOfMass.y.toPrecision(6)]; this.info['Center of Mass'] = [`${centerOfMass.x.toPrecision(6)}, `, centerOfMass.y.toPrecision(6)];
this.info['Net Angular Momentum'] = this.objects.computeSystemAngularMomentum().toPrecision(6); this.info['Net Angular Momentum'] = this.system.computeSystemAngularMomentum().toPrecision(6);
} }
this.objects.computeFrame(elapsedTime); this.display.frame(elapsedTime);
this.overlay.renderInfo(); this.system.frame(elapsedTime);
// this.display.computePanning(elapsedTime); this.overlay.frame();
this.display.fillCanvas();
this.display.drawObjects();
for (const group in this.toolbarGroups) { for (const group in this.toolbarGroups) {
this.toolbarGroups[group].frame(); this.toolbarGroups[group].frame();
} }
requestAnimationFrame(t => this.loop(t)); requestAnimationFrame(t => this.loop(t));
} }
} }

View File

@ -1,10 +1,11 @@
import { MassObject } from './object.js'; import { MassObject } from './object.js';
import { ZOOM_TO_FIT_PADDING } from './config.js'; import { ZOOM_TO_FIT_PADDING } from './config.js';
export class Objects { export class System {
objects = []; objects = [];
creatingObject = undefined; creatingObject = undefined;
selectedObject = undefined; selectedObject = undefined;
selectObjectStart = undefined;
paused = false; paused = false;
panVelocityPaused = undefined; panVelocityPaused = undefined;
@ -35,8 +36,9 @@ export class Objects {
// Create an object with mass that grows as pointer is held down // Create an object with mass that grows as pointer is held down
createObject(x, y) { createObject(x, y) {
const idx = this.objects.length; const idx = this.objects.length;
const obj = new MassObject(x, y, idx); const obj = new MassObject(this.sim, x, y);
this.creatingObject = idx; this.creatingObject = idx;
this.selectedObjectStart = {x, y, pointer: {x, y}};
this.objects.push(obj); this.objects.push(obj);
// Pause the simulation during mass creation; this avoids some complex local dynamics // Pause the simulation during mass creation; this avoids some complex local dynamics
if (this.sim.getOption('pauseDuring.creation')) { if (this.sim.getOption('pauseDuring.creation')) {
@ -59,8 +61,10 @@ export class Objects {
return this.objects[i]; return this.objects[i];
} }
selectObject(i) { selectObject(i, pointer) {
this.selectedObject = i; this.selectedObject = i;
const {x, y} = this.object(i).position;
this.selectedObjectStart = {x, y, pointer};
if (this.sim.getOption('pauseDuring.selection')) { if (this.sim.getOption('pauseDuring.selection')) {
this.pause(); this.pause();
} }
@ -68,6 +72,7 @@ export class Objects {
deselect() { deselect() {
this.selectedObject = undefined; this.selectedObject = undefined;
this.selectedObjectStart = undefined;
this.resume(); this.resume();
} }
@ -107,6 +112,7 @@ export class Objects {
objectAtLocation(x, y) { objectAtLocation(x, y) {
let idx = undefined; let idx = undefined;
this.selectedObjectStart = undefined;
this.forEachObject((obj, i) => { this.forEachObject((obj, i) => {
// If distance to object is less than object's radius, we are touching the object // 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); const dist = Math.pow((obj.position.x - x)**2 + (obj.position.y - y)**2, 1/2);
@ -123,7 +129,7 @@ export class Objects {
const touchingObject = this.objectAtLocation(x, y); const touchingObject = this.objectAtLocation(x, y);
if (touchingObject !== undefined) { if (touchingObject !== undefined) {
this.selectObject(touchingObject); this.selectObject(touchingObject, {x, y});
} else { } else {
// Otherwise, create a new object // Otherwise, create a new object
this.createObject(x, y); this.createObject(x, y);
@ -131,24 +137,31 @@ export class Objects {
} }
handlePointerUp() { handlePointerUp() {
const obj = this.getSelectedOrCreating();
if (obj === undefined) return;
this.doneCreatingObject(); this.doneCreatingObject();
this.deselect(); this.deselect();
// Convert pointer velocity to simulation scale
// Including time scale - if time is slow, our motion is relatively faster
const pointer = {...this.sim.pointer.latestVelocity};
obj.velocity.x = pointer.x / this.sim.display.scale * this.sim.timeScale;
obj.velocity.y = pointer.y / this.sim.display.scale * this.sim.timeScale;
if (this.sim.panning?.velocity) {
obj.velocity.x += this.sim.panning.velocity.x;
obj.velocity.y += this.sim.panning.velocity.y;
}
} }
handlePointerMove({x, y, vx, vy}) { handlePointerMove({x, y}) {
// If the cursor moves while creating an object, or while an object is selected, // If the cursor moves while creating an object, or while an object is selected,
// update the position and velocity of the object
// update the position using the pointer motion but the velocity using the pointer velocity // update the position using the pointer motion but the velocity using the pointer velocity
const obj = this.getSelectedOrCreating(); const obj = this.getSelectedOrCreating();
if (obj === undefined) return; if (obj === undefined) return;
if (this.sim.panning?.velocity) { const start = this.selectedObjectStart;
vx += this.sim.panning.velocity.x; obj.position.x = start.x + (x - start.pointer.x);
vy += this.sim.panning.velocity.y; obj.position.y = start.y + (y - start.pointer.y);
} obj.velocity.x = 0;
obj.position.x = x; obj.velocity.y = 0;
obj.position.y = y;
obj.velocity.x = vx;
obj.velocity.y = vy;
} }
// cb: (obj, idx) => {} // cb: (obj, idx) => {}
@ -163,6 +176,10 @@ export class Objects {
} }
} }
drawObjects() {
this.forEachObject(obj => obj.drawObject(this.sim), {alive: null});
}
// cb: (acc, obj, idx) => {} // cb: (acc, obj, idx) => {}
reduce(cb, initial, opts) { reduce(cb, initial, opts) {
let acc = initial; let acc = initial;
@ -201,9 +218,7 @@ export class Objects {
} }
// elapsedTime is given in milliseconds // elapsedTime is given in milliseconds
computeFrame(elapsedTime) { frame(elapsedTime) {
// convert elapsed time to seconds
elapsedTime /= 1000;
// If we're creating an object, increment its mass // If we're creating an object, increment its mass
// with the mass creation rate accelerating over time // with the mass creation rate accelerating over time
@ -211,8 +226,7 @@ export class Objects {
if (this.creatingObject !== undefined) { if (this.creatingObject !== undefined) {
const obj = this.objects[this.creatingObject]; const obj = this.objects[this.creatingObject];
// Putting in a somewhat arbitrary scaling factor here let massCreationRate = this.sim.getOption('param.massCreationRate');
let massCreationRate = this.sim.getOption('param.massCreationRate') / 1000;
// Mass creation rate acceleration // Mass creation rate acceleration
if (this.sim.getOption('param.massAcceleration')) { if (this.sim.getOption('param.massAcceleration')) {
massCreationRate *= obj.age; massCreationRate *= obj.age;
@ -224,8 +238,6 @@ export class Objects {
this.computeForces(); this.computeForces();
if (this.sim.playing) { if (this.sim.playing) {
// TODO: If creating/selected object, clamp its position to the cursor
// Predict positions (Velocity verlet method) // Predict positions (Velocity verlet method)
this.forEachObject(obj => { this.forEachObject(obj => {
obj.currentAcceleration = {...obj.acceleration}; obj.currentAcceleration = {...obj.acceleration};
@ -307,26 +319,39 @@ export class Objects {
} }
// Display objects info // Display objects info
// First clear info from previous frame
this.forEachObject((_obj, i) => {
delete this.sim.info[`Object ${i}`];
}, { alive: null });
if (this.sim.getOption('debug.objectsInfo')) { if (this.sim.getOption('debug.objectsInfo')) {
const aliveOnly = this.sim.getOption('debug.aliveObjects');
this.forEachObject((obj, i) => { this.forEachObject((obj, i) => {
const speed = Math.pow(obj.velocity.x ** 2 + obj.velocity.y ** 2, 1/2); const speed = Math.pow(obj.velocity.x ** 2 + obj.velocity.y ** 2, 1/2);
const accel = Math.pow(obj.acceleration.x ** 2 + obj.acceleration.y ** 2, 1/2);
// Invert y so that the angle is counterclockwise from x-axis // Invert y so that the angle is counterclockwise from x-axis
const direction = Math.atan2(-obj.velocity.y, obj.velocity.x) * 180 / Math.PI; const direction = Math.atan2(-obj.velocity.y, obj.velocity.x) * 180 / Math.PI;
const accelDir = Math.atan2(-obj.acceleration.y, obj.acceleration.x) * 180 / Math.PI;
const {r, g, b} = obj.color;
this.sim.info[`Object ${i}`] = [ this.sim.info[`Object ${i}`] = [
`${obj.position.x.toPrecision(6)}, `, `<span style="background-color: rgb(${r},${g},${b});">&nbsp;&nbsp;</span>`,
`${obj.position.y.toPrecision(6)}, `, `${obj.position.x.toPrecision(4)}, `,
`${obj.mass.toPrecision(6)} kg, `, `${obj.position.y.toPrecision(4)}, `,
`${obj.mass.toPrecision(4)} kg, `,
`${speed.toPrecision(2)} m/s, ${direction.toPrecision(2)}°`, `${speed.toPrecision(2)} m/s, ${direction.toPrecision(2)}°`,
`${accel.toPrecision(2)} m/s<sup>2</sup>, ${accelDir.toPrecision(2)}°`,
`Alive: ${obj.alive}`, `Alive: ${obj.alive}`,
]; ];
}, { alive: null }); }, { alive: aliveOnly || null });
} }
// Render the objects
this.drawObjects();
} }
computeSystemCenter() { computeSystemCenter() {
// Determine center of mass // Determine center of mass
const { totalMass, count, totalMassLocation } = const { totalMass, count, totalMassLocation } =
this.sim.objects.reduce((acc, obj) => ({ this.reduce((acc, obj) => ({
count: acc.count + 1, count: acc.count + 1,
totalMass: acc.totalMass + obj.mass, totalMass: acc.totalMass + obj.mass,
totalMassLocation: { totalMassLocation: {
@ -345,7 +370,7 @@ export class Objects {
} : {x: 0, y: 0}; } : {x: 0, y: 0};
// Determine average momentum // Determine average momentum
const netMomentum = this.sim.objects.reduce((acc, obj) => ({ const netMomentum = this.reduce((acc, obj) => ({
x: acc.x + obj.mass * obj.velocity.x, x: acc.x + obj.mass * obj.velocity.x,
y: acc.y + obj.mass * obj.velocity.y, y: acc.y + obj.mass * obj.velocity.y,
}), { x: 0, y: 0 }); }), { x: 0, y: 0 });

View File

@ -43,18 +43,11 @@ export class OptionsTool extends Tool {
const value = this.sim.getOption(path); const value = this.sim.getOption(path);
button.style.opacity = value ? '100%' : '50%'; button.style.opacity = value ? '100%' : '50%';
this.sim.onOptionSet(path, value => { this.sim.onOptionSet(path, value => {
console.log('option set cb', path, value);
button.style.opacity = value ? '100%' : '50%'; button.style.opacity = value ? '100%' : '50%';
console.log('button opacity', button.style.opacity);
});
button.addEventListener('click', () => {
const value = this.sim.options.getOption(path, true);
console.log('click, option value', value);
this.sim.setOption(path, !value);
}); });
button.addEventListener('click', () => { button.addEventListener('click', () => {
const value = this.sim.getOption(path); const value = this.sim.getOption(path);
this.setOption(path, !value); this.sim.setOption(path, !value);
}); });
return button; return button;
} }

View File

@ -81,14 +81,14 @@ export class UtilityTool extends Tool {
zeroVelocity.addEventListener('click', () => { zeroVelocity.addEventListener('click', () => {
// Determine center of mass and average momentum // Determine center of mass and average momentum
const { totalMass, netMomentum } = this.sim.objects.computeSystemCenter(); const { totalMass, netMomentum } = this.sim.system.computeSystemCenter();
const netVelocity = { const netVelocity = {
x: netMomentum.x / totalMass, x: netMomentum.x / totalMass,
y: netMomentum.y / totalMass, y: netMomentum.y / totalMass,
}; };
// Apply offset to all object velocities // Apply offset to all object velocities
this.sim.objects.forEachObject(obj => { this.sim.system.forEachObject(obj => {
obj.velocity.x -= netVelocity.x; obj.velocity.x -= netVelocity.x;
obj.velocity.y -= netVelocity.y; obj.velocity.y -= netVelocity.y;
}); });
@ -99,14 +99,14 @@ export class UtilityTool extends Tool {
clearTraces.addEventListener('click', () => { clearTraces.addEventListener('click', () => {
// Obliterate object histories // Obliterate object histories
this.sim.objects.forEachObject(obj => { this.sim.system.forEachObject(obj => {
obj.history = []; obj.history = [];
}, {alive: null}); }, {alive: null});
}); });
zoomAll.addEventListener('click', () => { zoomAll.addEventListener('click', () => {
// Determine bounding box // Determine bounding box
const box = this.sim.objects.boundingBox; const box = this.sim.system.boundingBox;
const x = (box.start.x + box.end.x) / 2; const x = (box.start.x + box.end.x) / 2;
const y = (box.start.y + box.end.y) / 2; const y = (box.start.y + box.end.y) / 2;
const widthRatio = Math.abs(box.start.x - box.end.x) / this.sim.display.width; const widthRatio = Math.abs(box.start.x - box.end.x) / this.sim.display.width;
@ -118,7 +118,7 @@ export class UtilityTool extends Tool {
const netMomentum = {x: 0, y: 0}; const netMomentum = {x: 0, y: 0};
let totalMass = 0; let totalMass = 0;
let count = 0; let count = 0;
this.sim.objects.forEachObject(obj => { this.sim.system.forEachObject(obj => {
count++; count++;
netMomentum.x += obj.mass * obj.velocity.x; netMomentum.x += obj.mass * obj.velocity.x;
netMomentum.y += obj.mass * obj.velocity.y; netMomentum.y += obj.mass * obj.velocity.y;