diff --git a/Readme.md b/Readme.md index 0f374ab..0b7a18d 100644 --- a/Readme.md +++ b/Readme.md @@ -16,13 +16,13 @@ TODO ---- - [x] Selection Box -- [ ] Feature: Object List -- [ ] Feature: Object Detail -- [ ] Feature: Zoom to Object +- [x] Feature: Object List +- [x] Feature: Object Detail +- [x] Feature: Zoom to Object - [ ] Feature: Teleport Object - [ ] Enhancement: Create Time class and refactor to use -- [ ] Enhancement: Create Vector class and refactor to use -- [ ] Enhancement: Create Panning class and refactor to use +- [x] Enhancement: Create Vector class and refactor to use +- [x] Enhancement: Create Panning class and refactor to use - [ ] Enhancement: Handle pointerleave or other mechanism when window loses focus - [ ] Enhancement: Calculate Work as FxD as measure of energy flux - [ ] Feature: Automatically slow time when energy flux is greater @@ -32,7 +32,7 @@ TODO - [ ] Feature: Polar Coordinates - [ ] Feature: Cylindrical Coordinates - [ ] Feature: Spherical Coordinates -- [ ] Feature: Camera Velocity Display +- [x] Feature: Camera Velocity Display - [ ] Enhancement: World State Snapshots - [ ] Feature: List / Save / Load World States - [ ] Feature: Left Button Panning @@ -52,5 +52,4 @@ TODO - [ ] Feature: Time Control: Reverse Time - [ ] Feature: Lossy Rescaling To Widen Zoom (Handling overflow/underflow) - [ ] Enhancement: Track farthest reaches, min/max in each dimension (x, y) -- [x] Task: Verify stationary pointer leads to zero pointer velocity -- [ ] Fix: Unpause panning when initiated while sim is paused +- [x] Fix: Unpause panning when initiated while sim is paused diff --git a/config.js b/config.js index c67d7f8..cb706d7 100644 --- a/config.js +++ b/config.js @@ -54,6 +54,9 @@ export const EVENT_MODE_ENTER = 'lhg-mode-enter'; export const EVENT_ZOOM = 'lhg-zoom-event'; export const EVENT_OPTION_SET = 'lhg-option-set'; export const EVENT_PLAY_PAUSE = 'lhg-play-pause'; +export const EVENT_SELECT = 'lhg-select'; +export const EVENT_OBJECT_CREATE = 'lhg-object-create'; +export const EVENT_OBJECT_MERGE = 'lhg-object-merge'; // MODES export const MODE_MASS_GENERATION = 'mass-gen'; diff --git a/helper.js b/helper.js index 79cd9b4..b3b9a0e 100644 --- a/helper.js +++ b/helper.js @@ -36,7 +36,8 @@ export function show({items, item, parentEl, itemEl}) { item.hidden = false; } -export function hide({item, parentEl, itemEl}) { +export function hide({items, item, parentEl, itemEl}) { + if (items.indexOf(item) < 0) return; parentEl.removeChild(itemEl); item.hidden = true; } diff --git a/pointer.js b/pointer.js index 479f734..2aaeca1 100644 --- a/pointer.js +++ b/pointer.js @@ -17,38 +17,38 @@ export class Pointer { this.sim = sim; // Monitor mouse movements - const el = this.sim.display.canvas; + const {canvas} = this.sim.display; - el.addEventListener('pointermove', e => { + window.addEventListener('pointermove', e => { if (this.sim.getOption('debug.cursorInfo')) { this.sim.info['pointermove'] = [`${e.clientX.toPrecision(6)}, `, `${e.clientY.toPrecision(6)}`]; } this.handlePointerMove({x: e.clientX, y: e.clientY}); }); - el.addEventListener('pointerdown', e => { + canvas.addEventListener('pointerdown', e => { this.handlePointerDown({x: e.clientX, y: e.clientY}); }); - el.addEventListener('pointerup', e => { + window.addEventListener('pointerup', e => { this.handlePointerUp({x: e.clientX, y: e.clientY}); }); - el.addEventListener('pointerleave', e => { - this.handlePointerUp({x: e.clientX, y: e.clientY}); - }); + // window.addEventListener('pointerleave', e => { + // this.handlePointerUp({x: e.clientX, y: e.clientY}); + // }); // Monitor wheel events - el.addEventListener('wheel', e => { + canvas.addEventListener('wheel', e => { 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); }); - el.addEventListener('focus', () => { + window.addEventListener('focus', () => { console.log('window focus'); }); - el.addEventListener('blur', () => { + window.addEventListener('blur', () => { console.log('window blur'); }); } diff --git a/select.js b/select.js index 29479ef..fd0e064 100644 --- a/select.js +++ b/select.js @@ -1,3 +1,4 @@ +import {EVENT_SELECT} from './config.js'; import {add, copy, mult} from './vector.js'; export class Select { @@ -30,7 +31,7 @@ export class Select { handlePointerDown({x: clientX, y: clientY}) { this.box.start = this.sim.screenToSim(clientX, clientY); this.box.end = this.box.start; - this.getSelectedObjects(); + // this.getSelectedObjects(); } handlePointerMove({x: clientX, y: clientY}) { @@ -51,22 +52,13 @@ export class Select { y: Math.max(start.y, end.y), }; this.getSelectedObjects(); + this.sim.div.dispatchEvent(new CustomEvent(EVENT_SELECT)); this.box = { start: undefined, end: undefined, }; } - getSelectedObjects() { - const {start, end} = this.box; - if (!start) return; - this.selectedGroup = this.sim.system.filter(({position: {x, y}}) => { - return x >= start.x && x <= end.x && y >= start.y && y <= end.y; - }); - // For now, first object in group is selected single - this.selectedSingle = this.selectedGroup[0] ?? undefined; - } - frame(elapsedTime) { if (!this.box.start) return; // If panning, let's update the position of our box so it doesn't drift away @@ -77,4 +69,22 @@ export class Select { // Display the box this.sim.display.drawBox(this.box.start, this.box.end); } + + getSelectedObjects() { + const {start, end} = this.box; + if (!start) return; + this.selectedGroup = this.sim.system.filter(({position: {x, y}}) => { + return x >= start.x && x <= end.x && y >= start.y && y <= end.y; + }); + // For now, first object in group is selected single + this.selectedSingle = this.selectedGroup[0] ?? undefined; + } + + // cb: ({selectedGroup, selectedSingle}) => undefined + onSelect(cb) { + this.sim.div.addEventListener(EVENT_SELECT, () => { + const {selectedGroup, selectedSingle} = this; + cb({selectedGroup, selectedSingle}); + }); + } } diff --git a/system.js b/system.js index 637fd34..518a2a1 100644 --- a/system.js +++ b/system.js @@ -1,4 +1,4 @@ -import {OBJECT_HISTORY_SIZE} from './config.js'; +import {EVENT_OBJECT_CREATE, EVENT_OBJECT_MERGE, OBJECT_HISTORY_SIZE} from './config.js'; import {MassObject} from './object.js'; import { add, copy, cross, degrees, @@ -134,6 +134,8 @@ export class System { }; T.alive = false; T.forces = []; + const e = new CustomEvent(EVENT_OBJECT_MERGE, {detail: {surviving: S, merged: T}}); + this.sim.div.dispatchEvent(e); } }, {alive: true, startWith: i + 1}); }); @@ -212,8 +214,9 @@ export class System { if (this.sim.getOption('pauseDuring.creation')) { this.pause(); } - obj.velocity = copy(this.sim.panning.velocity); + const e = new CustomEvent(EVENT_OBJECT_CREATE, {detail: {obj}}); + this.sim.div.dispatchEvent(e); } doneCreatingObject() { @@ -223,6 +226,20 @@ export class System { } } + // cb: (obj) => undefined + onCreate(cb) { + this.sim.div.addEventListener(EVENT_OBJECT_CREATE, ({detail: {obj}}) => { + cb(obj); + }); + } + + // cb: ({surviving, merged}) => undefined + onMerge(cb) { + this.sim.div.addEventListener(EVENT_OBJECT_MERGE, ({detail: {surviving, merged}}) => { + cb({surviving, merged}); + }); + } + object(i) { return this.objects[i]; } @@ -253,21 +270,20 @@ export class System { return this.objects.length; } - get boundingBox() { + getBoundingBox(objects = []) { const box = this.reduce(({start, end}, obj) => { + if (objects.length && !objects.includes(obj)) return {start, end}; const lx = obj.position.x - obj.radius; const gx = obj.position.x + obj.radius; const ly = obj.position.y - obj.radius; const gy = obj.position.y + obj.radius; - let ret; if (start.x === undefined) { - ret = { + return { start: {x: lx, y: ly}, end: {x: gx, y: gy}, }; - return ret; } - ret = { + return { start: { x: Math.min(start.x, lx), y: Math.min(start.y, ly), @@ -277,7 +293,6 @@ export class System { y: Math.max(end.y, gy), } }; - return ret; }, { start: {x: undefined, y: undefined}, end: {x: undefined, y: undefined}, @@ -373,15 +388,18 @@ export class System { }); } - computeSystemCenter() { + computeSystemCenter(objects = []) { // Determine center of mass const {totalMass, count, totalMassLocation} = - this.reduce((acc, obj) => ({ - count: acc.count + 1, - totalMass: acc.totalMass + obj.mass, - totalMassLocation: add(acc.totalMassLocation, - mult(obj.position, obj.mass)), - }), { + this.reduce((acc, obj) => { + if (objects.length && !objects.includes(obj)) return acc; + return { + count: acc.count + 1, + totalMass: acc.totalMass + obj.mass, + totalMassLocation: add(acc.totalMassLocation, + mult(obj.position, obj.mass)), + }; + }, { totalMassLocation: {x: 0, y: 0}, totalMass: 0, count: 0, @@ -390,8 +408,10 @@ export class System { const centerOfMass = count ? div(totalMassLocation, totalMass) : zero; // Determine average momentum - const netMomentum = this.reduce((acc, obj) => - add(acc, mult(obj.velocity, obj.mass)), zero); + const netMomentum = this.reduce((acc, obj) => { + if (objects.length && !objects.includes(obj)) return acc; + return add(acc, mult(obj.velocity, obj.mass)); + }, zero); return {totalMass, count, totalMassLocation, centerOfMass, netMomentum}; } diff --git a/tool/objects.js b/tool/objects.js index a7d61d7..b9c4585 100644 --- a/tool/objects.js +++ b/tool/objects.js @@ -1,11 +1,82 @@ +import {hide, show} from '../helper.js'; import {Tool} from '../tool.js'; +import {add, magnitude, sub} from '../vector.js'; export class ObjectsTool extends Tool { + objects = []; + setContainer(container) { super.setContainer(container); + + // Display a list of the currently selected objects, + // or all objects if none are currently selected. + if (this.sim.select.selectedGroup.length) { + this.objects = this.sim.select.selectedGroup; + } else { + this.objects = this.sim.system.filter(obj => obj.alive); + } + + this.populate(); + + this.sim.select.onSelect(({selectedGroup}) => { + this.objects = selectedGroup; + this.depopulate(); + this.populate(); + }); + + this.sim.system.onCreate(obj => { + if (!this.sim.select.selectedGroup.length) { + this.objects.push(obj); + this.populate(); + } + }); + + this.sim.system.onMerge(({merged}) => { + if (!merged.objectsToolEl) return; + hide({ + items: this.objects, + item: merged, + parentEl: this.div, + itemEl: merged.objectsToolEl, + }); + }); } - constructor() { - super(); + frame() { + this.populate(); + } + + depopulate() { + while (this.div.firstChild) { + this.div.removeChild(this.div.firstChild); + } + } + + populate() { + for (const obj of this.objects) { + const objectEl = obj.objectsToolEl ?? document.createElement('div'); + obj.objectsToolEl = objectEl; + const {r, g, b} = obj.color; + // Distance from center of screen + const distance = magnitude(sub(obj.position, add(this.sim.display.viewOrigin, { + x: this.sim.display.width / 2, + y: this.sim.display.height / 2, + }))); + objectEl.innerHTML = ` + ` + + '  ' + + `${obj.mass.toPrecision(3)} ` + + `${distance.toPrecision(3)}`; + // `${magnitude(obj.velocity).toExponential(0)} ` + + // `${-degrees(direction(obj.velocity)).toFixed(0)}°`; + if (!obj.hidden) { + show({ + items: this.objects, + item: obj, + parentEl: this.div, + itemEl: objectEl, + }); + } + } } } diff --git a/tool/options.js b/tool/options.js index 76027da..83ac97f 100644 --- a/tool/options.js +++ b/tool/options.js @@ -76,6 +76,7 @@ export class OptionsTool extends Tool { }); } else { hide({ + items: group.items, item: next, parentEl: groupEl, itemEl: optionEl, diff --git a/tool/utility.js b/tool/utility.js index 20d8c27..8c10baa 100644 --- a/tool/utility.js +++ b/tool/utility.js @@ -7,35 +7,6 @@ import { export class UtilityTool extends Tool { currentTimeEl = undefined; - get timeText() { - let time = this.sim.time; - // Time in milliseconds - const ms = Math.floor(time % 1000); - time = (time - ms) / 1000; - const s = Math.floor(time % 60); - time = (time - s) / 60; - const m = Math.floor(time % 60); - time = (time - m) / 60; - const h = Math.floor(time % 24); - time = (time - h) / 24; - const d = Math.floor(time); - return [ - d || undefined, - h.toString().padStart(2, '0'), - m.toString().padStart(2, '0'), - [ - s.toString().padStart(2, '0'), - ms.toString().padStart(3, '0'), - ].join('.') - ].filter(x => x !== undefined).join(':'); - } - - frame() { - if (this.currentTimeEl) { - this.currentTimeEl.innerHTML = this.timeText; - } - } - setContainer(container) { super.setContainer(container); this.currentTimeEl.innerHTML = this.timeText; @@ -73,4 +44,34 @@ export class UtilityTool extends Tool { this.sim.info = {}; }); } + + frame() { + if (this.currentTimeEl) { + this.currentTimeEl.innerHTML = this.timeText; + } + } + + get timeText() { + let time = this.sim.time; + // Time in milliseconds + const ms = Math.floor(time % 1000); + time = (time - ms) / 1000; + const s = Math.floor(time % 60); + time = (time - s) / 60; + const m = Math.floor(time % 60); + time = (time - m) / 60; + const h = Math.floor(time % 24); + time = (time - h) / 24; + const d = Math.floor(time); + return [ + d || undefined, + h.toString().padStart(2, '0'), + m.toString().padStart(2, '0'), + [ + s.toString().padStart(2, '0'), + ms.toString().padStart(3, '0'), + ].join('.') + ].filter(x => x !== undefined).join(':'); + } + } diff --git a/tool/zoom.js b/tool/zoom.js index f01e8ba..0c718e8 100644 --- a/tool/zoom.js +++ b/tool/zoom.js @@ -65,7 +65,8 @@ export class Zoom extends Tool { zoomAll.addEventListener('click', () => { // Determine bounding box - const box = this.sim.system.boundingBox; + const objects = this.sim.select.selectedGroup; + const box = this.sim.system.getBoundingBox(objects); const x = (box.start.x + box.end.x) / 2; const y = (box.start.y + box.end.y) / 2; const widthRatio = Math.abs(box.start.x - box.end.x) / this.sim.display.width; @@ -74,7 +75,7 @@ export class Zoom extends Tool { const factor = Math.ceil(Math.log2(1 / ratio)); // Determine average momentum and set panning velocity to match - const {netMomentum, totalMass} = this.sim.system.computeSystemCenter(); + const {netMomentum, totalMass} = this.sim.system.computeSystemCenter(objects); const netVelocity = { x: netMomentum.x / totalMass, y: netMomentum.y / totalMass,